@indigoai-us/hq-cli 5.193.0 → 5.194.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 CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.194.0] — 2026-09-25
6
+
7
+ ### Added
8
+
9
+ - `hq core checkpoint` can now maintain the company ontology, the job the
10
+ cloud ontology gardener did. It is off unless the hq-flags flag
11
+ `checkpoint.ontology` is on for the checkpoint's company
12
+ (`HQ_FLAG_CHECKPOINT_ONTOLOGY` is the local override). When a checkpoint
13
+ names a company, the background sibling runs `hq core ontology plan`, which
14
+ checks the flag and lists the company knowledge and signal files that
15
+ changed since the last run, within a fixed input and token budget. The
16
+ sibling writes entity candidates for those files, and
17
+ `hq core ontology apply` validates them and writes them. It updates an
18
+ existing entity rather than creating a duplicate, and keeps entity ids in
19
+ the cloud's format. It writes only to the checkpoint's own company and never
20
+ follows a symlink. It adds a back-reference to each signal that names an
21
+ entity, skipping signals the gardener already linked, and a rerun changes
22
+ nothing. Files under `@{key}` audience folders are not read.
23
+
24
+ ## [5.193.1] — 2026-09-25
25
+
26
+ ### Fixed
27
+
28
+ - Work Mesh project sync maps ledger states (`todo` to `queued`, `in_progress` to
29
+ `in_progress`, `in_review`/`verification`/`blocked` to `review`, and `done`/`dropped`
30
+ to `done`), with explicit `status` taking precedence and `passes` preserved. PRD
31
+ sync emits review transitions; Board readers preserve unknown status strings and
32
+ ignore unknown ledger states. `hq mesh project ensure --dry-run` previews merged
33
+ Board statuses without writing.
34
+ - Claude lane balance reports now normalize Outpost lane-list and queued show
35
+ views before strict lane-record validation, including `no_story_reason: null`.
36
+
5
37
  ## [5.193.0] — 2026-09-25
6
38
 
7
39
  ### Fixed
@@ -5101,6 +5101,45 @@ export declare const COMMAND_CATALOG: readonly [{
5101
5101
  readonly description: "print the planned checkpoint without writing";
5102
5102
  }];
5103
5103
  readonly subcommands: readonly [];
5104
+ }, {
5105
+ readonly name: "ontology";
5106
+ readonly description: "Checkpoint ontology steps run by the checkpoint sibling (gated by checkpoint.ontology)";
5107
+ readonly aliases: readonly [];
5108
+ readonly hidden: false;
5109
+ readonly usage: "[options] [command]";
5110
+ readonly arguments: readonly [];
5111
+ readonly options: readonly [];
5112
+ readonly subcommands: readonly [{
5113
+ readonly name: "plan";
5114
+ readonly description: "Check the flag and list the changed company files for this checkpoint run";
5115
+ readonly aliases: readonly [];
5116
+ readonly hidden: false;
5117
+ readonly usage: "[options]";
5118
+ readonly arguments: readonly [];
5119
+ readonly options: readonly [{
5120
+ readonly flags: "--run-dir <path>";
5121
+ readonly description: "checkpoint sibling run directory holding payload.json";
5122
+ }, {
5123
+ readonly flags: "--hq-root <path>";
5124
+ readonly description: "HQ installation to operate on";
5125
+ }];
5126
+ readonly subcommands: readonly [];
5127
+ }, {
5128
+ readonly name: "apply";
5129
+ readonly description: "Validate the run's entity candidates and write them to the payload company";
5130
+ readonly aliases: readonly [];
5131
+ readonly hidden: false;
5132
+ readonly usage: "[options]";
5133
+ readonly arguments: readonly [];
5134
+ readonly options: readonly [{
5135
+ readonly flags: "--run-dir <path>";
5136
+ readonly description: "checkpoint sibling run directory holding the plan and candidates";
5137
+ }, {
5138
+ readonly flags: "--hq-root <path>";
5139
+ readonly description: "HQ installation to operate on";
5140
+ }];
5141
+ readonly subcommands: readonly [];
5142
+ }];
5104
5143
  }, {
5105
5144
  readonly name: "generate-workers-registry";
5106
5145
  readonly description: "Regenerate the derived workers registry";
@@ -5777,6 +5816,9 @@ export declare const COMMAND_CATALOG: readonly [{
5777
5816
  }, {
5778
5817
  readonly flags: "--stories-file <path>";
5779
5818
  readonly description: "PRD JSON file with userStories and metadata.repos";
5819
+ }, {
5820
+ readonly flags: "--dry-run";
5821
+ readonly description: "Read the current Board and print computed story statuses without writing";
5780
5822
  }, {
5781
5823
  readonly flags: "--json";
5782
5824
  readonly description: "Print {projectId, threadId, channelId, version, stories}";
@@ -6598,6 +6598,55 @@ export const COMMAND_CATALOG = [
6598
6598
  ],
6599
6599
  "subcommands": []
6600
6600
  },
6601
+ {
6602
+ "name": "ontology",
6603
+ "description": "Checkpoint ontology steps run by the checkpoint sibling (gated by checkpoint.ontology)",
6604
+ "aliases": [],
6605
+ "hidden": false,
6606
+ "usage": "[options] [command]",
6607
+ "arguments": [],
6608
+ "options": [],
6609
+ "subcommands": [
6610
+ {
6611
+ "name": "plan",
6612
+ "description": "Check the flag and list the changed company files for this checkpoint run",
6613
+ "aliases": [],
6614
+ "hidden": false,
6615
+ "usage": "[options]",
6616
+ "arguments": [],
6617
+ "options": [
6618
+ {
6619
+ "flags": "--run-dir <path>",
6620
+ "description": "checkpoint sibling run directory holding payload.json"
6621
+ },
6622
+ {
6623
+ "flags": "--hq-root <path>",
6624
+ "description": "HQ installation to operate on"
6625
+ }
6626
+ ],
6627
+ "subcommands": []
6628
+ },
6629
+ {
6630
+ "name": "apply",
6631
+ "description": "Validate the run's entity candidates and write them to the payload company",
6632
+ "aliases": [],
6633
+ "hidden": false,
6634
+ "usage": "[options]",
6635
+ "arguments": [],
6636
+ "options": [
6637
+ {
6638
+ "flags": "--run-dir <path>",
6639
+ "description": "checkpoint sibling run directory holding the plan and candidates"
6640
+ },
6641
+ {
6642
+ "flags": "--hq-root <path>",
6643
+ "description": "HQ installation to operate on"
6644
+ }
6645
+ ],
6646
+ "subcommands": []
6647
+ }
6648
+ ]
6649
+ },
6601
6650
  {
6602
6651
  "name": "generate-workers-registry",
6603
6652
  "description": "Regenerate the derived workers registry",
@@ -7458,6 +7507,10 @@ export const COMMAND_CATALOG = [
7458
7507
  "flags": "--stories-file <path>",
7459
7508
  "description": "PRD JSON file with userStories and metadata.repos"
7460
7509
  },
7510
+ {
7511
+ "flags": "--dry-run",
7512
+ "description": "Read the current Board and print computed story statuses without writing"
7513
+ },
7461
7514
  {
7462
7515
  "flags": "--json",
7463
7516
  "description": "Print {projectId, threadId, channelId, version, stories}"
@@ -40,8 +40,19 @@ export declare const CHECKPOINT_IDLE_REPLY_REMINDER: string;
40
40
  * Kept in TypeScript rather than in a bundled asset: it is an instruction to
41
41
  * a locally-installed agent, not a scaffold script that should be packaged.
42
42
  */
43
- export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)\n both for session context and to extract additional reusable learnings/insights\n the parent did not pass explicitly. Never quote secrets\n or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists\n under this HQ root, read it and follow it wherever it goes beyond these instructions;\n the write bounds below always win over the skill text.\n2. Upgrade the thread file named in the payload IN PLACE: verify/repair its\n JSON; fill git.remote_url, git.initial_commit, git.commits_made, and\n git.knowledge_repos by scanning core/knowledge/public/*,\n core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge\n for symlinks or directories containing .git, recording dirty repositories\n as {\"<name>\": {\"commit\": \"<short>\", \"dirty\": true}}. Fill worker,\n next_steps, and insights; set type to \"checkpoint\"; then rename the file to\n drop -auto- from its filename. Use the renamed path in every reference you\n write afterwards.\n3. For every explicit or transcript-derived learning that is a reusable rule,\n FIRST search the existing policies for one the learning refines, contradicts\n or duplicates. Then take exactly one of these actions and name it in the\n report \u2014 the policy set is curated, not append-only:\n - AMEND an existing policy in place when the learning sharpens it, narrows\n its scope, or adds a case, and the rule as written is still correct.\n - SUPERSEDE it when the learning contradicts it: rewrite the rule to what is\n now true and record inside the file what changed and why.\n - MERGE near-duplicates into the single best-named file, then delete the\n files you merged away.\n - CREATE a new policy only when no existing policy covers the rule.\n Follow core/knowledge/public/hq-core/policies-spec.md. Write under\n personal/policies/ or, only when the payload names a company and the rule is\n company-specific, companies/<company>/policies/.\n DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,\n only inside those two directories, and never one whose body marks it HARD \u2014\n if a hard policy now looks wrong, leave it untouched and flag it in the\n report for a human to decide. Never delete a file you have not read.\n Apply the same curation to durable facts (not rules) under\n personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact\n in place rather than appending a second, contradictory copy of it.\n Store up to two explicit or transcript-derived insights per\n core/knowledge/public/hq-core/insights-spec.md when present, otherwise\n workspace/insights/.\n4. Close an active session journal fail-soft with\n bash .claude/skills/_shared/journal.sh close \"<project_dir>\" \"<one-line synthesis>\".\n Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with\n id, created_at, summary, files, and next_steps for backward compatibility.\n5. Update workspace/threads/recent.md and regenerate\n workspace/threads/INDEX.md. For each company whose knowledge path appears\n in files_touched, regenerate companies/<company>/knowledge/INDEX.md under\n core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation\n is allowed for those companies, but knowledge/policy content writes remain\n restricted to the payload's named company.\n6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;\n skip silently on any failure. Hook or automation improvements go ONLY under\n personal/hooks/ as proposals.\n7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.\n8. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), and what you skipped and why. List every policy or knowledge file\n you amended, superseded, merged or deleted with the reason, so a human can\n audit and reverse it; a deletion you do not name in the report is a defect.\n Then drain the queue: while\n workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim\n it atomically by renaming it aside \u2014\n mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl\n (N counting up from 2) \u2014 and process the claimed payloads with this same\n flow. Repeat until a claim finds nothing left, then update the report.\n NEVER read the queue and truncate it in place: a payload appended between\n your read and the truncate is lost, and its checkpoint is never enriched.\n";
44
- export declare function renderSiblingPrompt(runDir: string, payloadPath: string): string;
43
+ export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)\n both for session context and to extract additional reusable learnings/insights\n the parent did not pass explicitly. Never quote secrets\n or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists\n under this HQ root, read it and follow it wherever it goes beyond these instructions;\n the write bounds below always win over the skill text.\n2. Upgrade the thread file named in the payload IN PLACE: verify/repair its\n JSON; fill git.remote_url, git.initial_commit, git.commits_made, and\n git.knowledge_repos by scanning core/knowledge/public/*,\n core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge\n for symlinks or directories containing .git, recording dirty repositories\n as {\"<name>\": {\"commit\": \"<short>\", \"dirty\": true}}. Fill worker,\n next_steps, and insights; set type to \"checkpoint\"; then rename the file to\n drop -auto- from its filename. Use the renamed path in every reference you\n write afterwards.\n3. For every explicit or transcript-derived learning that is a reusable rule,\n FIRST search the existing policies for one the learning refines, contradicts\n or duplicates. Then take exactly one of these actions and name it in the\n report \u2014 the policy set is curated, not append-only:\n - AMEND an existing policy in place when the learning sharpens it, narrows\n its scope, or adds a case, and the rule as written is still correct.\n - SUPERSEDE it when the learning contradicts it: rewrite the rule to what is\n now true and record inside the file what changed and why.\n - MERGE near-duplicates into the single best-named file, then delete the\n files you merged away.\n - CREATE a new policy only when no existing policy covers the rule.\n Follow core/knowledge/public/hq-core/policies-spec.md. Write under\n personal/policies/ or, only when the payload names a company and the rule is\n company-specific, companies/<company>/policies/.\n DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,\n only inside those two directories, and never one whose body marks it HARD \u2014\n if a hard policy now looks wrong, leave it untouched and flag it in the\n report for a human to decide. Never delete a file you have not read.\n Apply the same curation to durable facts (not rules) under\n personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact\n in place rather than appending a second, contradictory copy of it.\n Store up to two explicit or transcript-derived insights per\n core/knowledge/public/hq-core/insights-spec.md when present, otherwise\n workspace/insights/.\n4. Close an active session journal fail-soft with\n bash .claude/skills/_shared/journal.sh close \"<project_dir>\" \"<one-line synthesis>\".\n Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with\n id, created_at, summary, files, and next_steps for backward compatibility.\n5. Update workspace/threads/recent.md and regenerate\n workspace/threads/INDEX.md. For each company whose knowledge path appears\n in files_touched, regenerate companies/<company>/knowledge/INDEX.md under\n core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation\n is allowed for those companies, but knowledge/policy content writes remain\n restricted to the payload's named company.\n<ontologyStep>6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;\n skip silently on any failure. Hook or automation improvements go ONLY under\n personal/hooks/ as proposals.\n7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.\n8. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), and what you skipped and why. List every policy or knowledge file\n you amended, superseded, merged or deleted with the reason, so a human can\n audit and reverse it; a deletion you do not name in the report is a defect.\n Then drain the queue: while\n workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim\n it atomically by renaming it aside \u2014\n mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl\n (N counting up from 2) \u2014 and process the claimed payloads with this same\n flow. Repeat until a claim finds nothing left, then update the report.\n NEVER read the queue and truncate it in place: a payload appended between\n your read and the truncate is lost, and its checkpoint is never enriched.\n";
44
+ /**
45
+ * Rendered into the sibling prompt only when the payload names a company. The
46
+ * flag check happens inside `hq core ontology plan`, in the sibling, because
47
+ * the checkpoint parent performs no network I/O. Only this run's own payload
48
+ * gets the step; claimed queue payloads are covered by the next run, since the
49
+ * plan works from file hashes rather than from the payload's file list.
50
+ */
51
+ export declare const SIBLING_ONTOLOGY_STEP = "5b. ONTOLOGY (this run's own payload only; skip it for claimed queue payloads).\n Run: hq core ontology plan --run-dir \"<runDir>\" --hq-root \"<hqRoot>\"\n If it prints \"ontology: skipped\", do nothing more for this step. Otherwise\n read <runDir>/ontology-plan.json, follow its \"instructions\" exactly, and\n write <runDir>/ontology-candidates.json. Read only the files and line\n ranges the plan lists. Then run:\n hq core ontology apply --run-dir \"<runDir>\" --hq-root \"<hqRoot>\"\n Never write under the company's ontology/ folder yourself, and never edit\n payload.json or ontology-plan.json. Copy the command's one-line output into\n the report. If either command fails, record the error and continue.\n";
52
+ export declare function renderSiblingPrompt(runDir: string, payloadPath: string, options?: {
53
+ company?: string | null;
54
+ hqRoot?: string;
55
+ }): string;
45
56
  export type WriteStampsOptions = {
46
57
  /**
47
58
  * Filesystem seam (default the real `node:fs`); injected in tests to force a
@@ -165,7 +165,7 @@ report instead of guessing.
165
165
  core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation
166
166
  is allowed for those companies, but knowledge/policy content writes remain
167
167
  restricted to the payload's named company.
168
- 6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;
168
+ <ontologyStep>6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;
169
169
  skip silently on any failure. Hook or automation improvements go ONLY under
170
170
  personal/hooks/ as proposals.
171
171
  7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.
@@ -182,10 +182,31 @@ report instead of guessing.
182
182
  NEVER read the queue and truncate it in place: a payload appended between
183
183
  your read and the truncate is lost, and its checkpoint is never enriched.
184
184
  `;
185
- export function renderSiblingPrompt(runDir, payloadPath) {
185
+ /**
186
+ * Rendered into the sibling prompt only when the payload names a company. The
187
+ * flag check happens inside `hq core ontology plan`, in the sibling, because
188
+ * the checkpoint parent performs no network I/O. Only this run's own payload
189
+ * gets the step; claimed queue payloads are covered by the next run, since the
190
+ * plan works from file hashes rather than from the payload's file list.
191
+ */
192
+ export const SIBLING_ONTOLOGY_STEP = `5b. ONTOLOGY (this run's own payload only; skip it for claimed queue payloads).
193
+ Run: hq core ontology plan --run-dir "<runDir>" --hq-root "<hqRoot>"
194
+ If it prints "ontology: skipped", do nothing more for this step. Otherwise
195
+ read <runDir>/ontology-plan.json, follow its "instructions" exactly, and
196
+ write <runDir>/ontology-candidates.json. Read only the files and line
197
+ ranges the plan lists. Then run:
198
+ hq core ontology apply --run-dir "<runDir>" --hq-root "<hqRoot>"
199
+ Never write under the company's ontology/ folder yourself, and never edit
200
+ payload.json or ontology-plan.json. Copy the command's one-line output into
201
+ the report. If either command fails, record the error and continue.
202
+ `;
203
+ export function renderSiblingPrompt(runDir, payloadPath, options = {}) {
204
+ const ontologyStep = options.company && options.hqRoot ? SIBLING_ONTOLOGY_STEP : "";
186
205
  return SIBLING_PROMPT_TEMPLATE
206
+ .replaceAll("<ontologyStep>", () => ontologyStep)
187
207
  .replaceAll("<runDir>", () => runDir)
188
- .replaceAll("<payloadPath>", () => payloadPath);
208
+ .replaceAll("<payloadPath>", () => payloadPath)
209
+ .replaceAll("<hqRoot>", () => options.hqRoot ?? "");
189
210
  }
190
211
  function collect(value, previous) {
191
212
  return [...previous, value];
@@ -946,7 +967,7 @@ function startSibling(liveRoot, input, threadPath, backend) {
946
967
  payload.pending_payloads = claimPending(pendingPath, path.join(runDir, "pending-claimed.jsonl"));
947
968
  const payloadPath = path.join(runDir, "payload.json");
948
969
  fs.writeFileSync(payloadPath, `${JSON.stringify(payload, null, 2)}\n`);
949
- const prompt = renderSiblingPrompt(runDir, payloadPath);
970
+ const prompt = renderSiblingPrompt(runDir, payloadPath, { company: payload.company, hqRoot: liveRoot });
950
971
  fs.writeFileSync(path.join(runDir, "prompt.md"), prompt);
951
972
  const logPath = path.join(runDir, "run.log");
952
973
  const logFd = fs.openSync(logPath, "a");
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `hq core ontology plan|apply` — the checkpoint sibling's ontology steps.
3
+ *
4
+ * The checkpoint parent never runs these: `plan` reads the hq-flags registry,
5
+ * which is network I/O. The detached sibling runs `plan`, writes candidates
6
+ * from the files the plan lists, then runs `apply`. Both commands take the
7
+ * company from the run's `payload.json`, never from the agent, so an agent
8
+ * cannot point the writes at another company.
9
+ */
10
+ import { Command } from "commander";
11
+ import { type FlagReader } from "../lib/checkpoint-ontology/plan.js";
12
+ export declare const ONTOLOGY_PAYLOAD_FILE = "payload.json";
13
+ /** The hq-flags read, scoped to the payload company. */
14
+ export declare const registryFlagReader: FlagReader;
15
+ export interface OntologyCommandDeps {
16
+ readFlag?: FlagReader;
17
+ now?: () => Date;
18
+ }
19
+ export declare function runOntologyPlan(hqRoot: string, runDirOption: string | undefined, deps?: OntologyCommandDeps): Promise<string>;
20
+ export declare function runOntologyApply(hqRoot: string, runDirOption: string | undefined, deps?: OntologyCommandDeps): string;
21
+ export declare function registerCoreOntologyCommand(core: Command): void;
22
+ //# sourceMappingURL=core-ontology.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `hq core ontology plan|apply` — the checkpoint sibling's ontology steps.
3
+ *
4
+ * The checkpoint parent never runs these: `plan` reads the hq-flags registry,
5
+ * which is network I/O. The detached sibling runs `plan`, writes candidates
6
+ * from the files the plan lists, then runs `apply`. Both commands take the
7
+ * company from the run's `payload.json`, never from the agent, so an agent
8
+ * cannot point the writes at another company.
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
13
+ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
14
+ import { planOntologyRun } from "../lib/checkpoint-ontology/plan.js";
15
+ import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.js";
16
+ import { WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
17
+ export const ONTOLOGY_PAYLOAD_FILE = "payload.json";
18
+ class OntologyUsageError extends Error {
19
+ }
20
+ function readPayload(runDir) {
21
+ const file = path.join(runDir, ONTOLOGY_PAYLOAD_FILE);
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
25
+ }
26
+ catch (error) {
27
+ throw new OntologyUsageError(`ontology: cannot read ${file} (${error.name})`);
28
+ }
29
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
30
+ throw new OntologyUsageError(`ontology: ${file} is not a JSON object`);
31
+ }
32
+ return raw;
33
+ }
34
+ function requireRunDir(runDir) {
35
+ if (!runDir)
36
+ throw new OntologyUsageError("ontology: --run-dir is required");
37
+ const resolved = path.resolve(runDir);
38
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
39
+ throw new OntologyUsageError(`ontology: run directory not found: ${resolved}`);
40
+ }
41
+ return resolved;
42
+ }
43
+ /** The hq-flags read, scoped to the payload company. */
44
+ export const registryFlagReader = (flagKey, company) => readLanesRegistryFlag(flagKey, {
45
+ company: company.cloudUid ? { slug: company.slug, uid: company.cloudUid } : null,
46
+ });
47
+ export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
48
+ const runDir = requireRunDir(runDirOption);
49
+ const outcome = await planOntologyRun({
50
+ hqRoot,
51
+ runDir,
52
+ payload: readPayload(runDir),
53
+ readFlag: deps.readFlag ?? registryFlagReader,
54
+ now: deps.now?.(),
55
+ });
56
+ if (outcome.status === "skipped")
57
+ return `ontology: skipped (${outcome.reason})`;
58
+ const { plan } = outcome;
59
+ return (`ontology: planned ${plan.inputs.length} input(s), ~${plan.budget.planned_tokens} tokens, ` +
60
+ `${plan.pending_after_run} left for later runs; plan at ${outcome.planPath}`);
61
+ }
62
+ export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
63
+ const runDir = requireRunDir(runDirOption);
64
+ const company = readPayload(runDir).company;
65
+ if (typeof company !== "string" || company === "") {
66
+ throw new OntologyUsageError("ontology: the run payload names no company");
67
+ }
68
+ const report = applyOntologyRun({ hqRoot, runDir, company, now: deps.now?.() });
69
+ if (report.status !== "applied")
70
+ return `ontology: ${report.status.replaceAll("_", " ")}; nothing written`;
71
+ return (`ontology: ${report.created.length} created, ${report.updated.length} updated, ` +
72
+ `${report.matched} matched, ${report.rejected.length} rejected, ${report.deferred.length} deferred, ` +
73
+ `${report.suspect_duplicates.length} suspect duplicate(s), ${report.write_errors.length} write error(s)`);
74
+ }
75
+ function fail(error) {
76
+ if (error instanceof OntologyUsageError || error instanceof ApplyError || error instanceof WriteBoundsError) {
77
+ process.stderr.write(`${error.message}\n`);
78
+ process.exit(1);
79
+ }
80
+ throw error;
81
+ }
82
+ export function registerCoreOntologyCommand(core) {
83
+ const ontology = core
84
+ .command("ontology")
85
+ .description("Checkpoint ontology steps run by the checkpoint sibling (gated by checkpoint.ontology)");
86
+ const rootFor = (options) => resolveLiveRoot({ hqRoot: options.hqRoot ?? core.opts().hqRoot });
87
+ ontology
88
+ .command("plan")
89
+ .description("Check the flag and list the changed company files for this checkpoint run")
90
+ .option("--run-dir <path>", "checkpoint sibling run directory holding payload.json")
91
+ .option("--hq-root <path>", "HQ installation to operate on")
92
+ .action(async (options) => {
93
+ try {
94
+ process.stdout.write(`${await runOntologyPlan(rootFor(options), options.runDir)}\n`);
95
+ }
96
+ catch (error) {
97
+ fail(error);
98
+ }
99
+ });
100
+ ontology
101
+ .command("apply")
102
+ .description("Validate the run's entity candidates and write them to the payload company")
103
+ .option("--run-dir <path>", "checkpoint sibling run directory holding the plan and candidates")
104
+ .option("--hq-root <path>", "HQ installation to operate on")
105
+ .action((options) => {
106
+ try {
107
+ process.stdout.write(`${runOntologyApply(rootFor(options), options.runDir)}\n`);
108
+ }
109
+ catch (error) {
110
+ fail(error);
111
+ }
112
+ });
113
+ }
114
+ //# sourceMappingURL=core-ontology.js.map
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { Option } from "commander";
32
32
  import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
33
+ import { registerCoreOntologyCommand } from "./core-ontology.js";
33
34
  import { resolveLiveRoot } from "../utils/hq-roots.js";
34
35
  import { runBundledScript } from "../utils/run-bundled-script.js";
35
36
  import { renderIndexTarget } from "../lib/index-render/index.js";
@@ -322,6 +323,7 @@ export function registerCoreCommands(program) {
322
323
  // This group primarily hosts manifest-driven bundled assets, but it also
323
324
  // hosts native TypeScript plumbing when a scaffold contract needs it.
324
325
  registerCoreCheckpointCommand(core);
326
+ registerCoreOntologyCommand(core);
325
327
  // These command names intentionally own native implementations. Their
326
328
  // similarly named scaffold assets are forwarders, so sending them through
327
329
  // runEntry would recurse back into this command forever.
@@ -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