@bussolabs/closeyourit-cli 0.15.0 → 0.17.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/README.md CHANGED
@@ -105,6 +105,9 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
105
105
  | `tickets watch\|unwatch <id> --project <id\|key>` | Watch / stop watching a ticket (its notifications). |
106
106
  | `tickets branch <id\|code> --project <id\|key>` | Create a GitHub branch from a ticket (name prefixed with the ticket code). |
107
107
  | `tickets pr <id\|code> --project <id\|key>` | Open a GitHub pull request from a ticket (head = the ticket branch). |
108
+ | `tickets lease claim <id\|code> --project <id\|key> [--ttl <seconds>] [--run-id <id>]` | Claim the working lease on a ticket (server default TTL: 8h). Exits non-zero on a 409, naming the current holder and its expiry. |
109
+ | `tickets lease renew <id\|code> --project <id\|key> [--ttl <seconds>] [--run-id <id>]` | Extend your own lease before it expires. |
110
+ | `tickets lease release <id\|code> --project <id\|key> [--run-id <id>]` | Release your own lease so someone else can claim the ticket. |
108
111
  | `ideas delete <id> --project <id\|key> --confirm` | Delete an idea (author always allowed; others need ideas.delete). |
109
112
  | `ideas archive\|unarchive <id> --project <id\|key>` | Archive an idea (open → archived) / reopen it. |
110
113
  | `ideas case-update <idea-id> <case-id> --project <id\|key> [--title] [--description]` | Update a case (example) of an idea. |
@@ -115,11 +118,12 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
115
118
  | `logs show <id>` | Show a single log entry (attributes, trace id, correlation). |
116
119
  | `logs link <id> --to <Errors::Group:uuid\|Ticketing::Ticket:uuid>` | Manually link a log entry to an error group or ticket. |
117
120
  | `logs unlink <id> <link-id> --confirm` | Remove a manual log link. |
118
- | `kb list [--project <id\|key>] [--kind] [--per] [--page]` | List knowledge pages. Cross-project: omit `--project` for every visible project. |
121
+ | `kb list [--project <id\|key>] [--kind] [--status] [--awaiting-consolidation] [--per] [--page]` | List knowledge pages. Cross-project: omit `--project` for every visible project. Published only unless `--status` says otherwise. |
119
122
  | `kb search <query…> [--project <id\|key>] [--kind] [--per] [--page] [--with-related]` | Search knowledge pages (semantic when available, title ILIKE fallback). |
120
123
  | `kb show <id> [--related] [--question <q>]` | Show one knowledge page: metadata header, raw markdown body, then the technical section. |
121
124
  | `kb related <id> [--question <q>] [--links-only]` | Pages to read next: linked with `[[wiki links]]` plus close matches by meaning. |
122
- | `kb create --project <id\|key> --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>) [--tech-spec <md> \| --tech-spec-file <path>]` | Create a knowledge page (optional technical section, kept separate from the body). |
125
+ | `kb create --project <id\|key> --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>) [--tech-spec <md> \| --tech-spec-file <path>] [--in-review] [--review-note <line>]` | Create a knowledge page (optional technical section, kept separate from the body). With `--in-review` the page waits for a human and stays out of search, answers and related panels. |
126
+ | `kb consolidate <id> --path <doc-path>` | Mark an accepted page as written to the versioned docs, recording where the document lives. |
123
127
  | `kb update <id> [--title] [--kind] [--body \| --body-file] [--tech-spec \| --tech-spec-file]` | Update a page (unpassed fields keep their current value). |
124
128
  | `kb delete <id> --confirm` | Delete a knowledge page (irreversible). |
125
129
  | `kb ask <question…>` | Ask a question; the AI answers from the knowledge base and cites the pages it used. |
@@ -0,0 +1,17 @@
1
+ import { BaseCommand } from '../../base';
2
+ /**
3
+ * Second step of the review flow: the accepted page has also been written as a versioned document
4
+ * in the knowledge-base repo, so record where it lives. Accepting and discarding stay human gestures
5
+ * on the web review page — this command only closes the loop the CLI can close by itself.
6
+ */
7
+ export default class KbConsolidate extends BaseCommand {
8
+ static args: {
9
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
10
+ };
11
+ static description: string;
12
+ static examples: string[];
13
+ static flags: {
14
+ path: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<unknown>;
17
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ const output_1 = require("../../lib/output");
6
+ /**
7
+ * Second step of the review flow: the accepted page has also been written as a versioned document
8
+ * in the knowledge-base repo, so record where it lives. Accepting and discarding stay human gestures
9
+ * on the web review page — this command only closes the loop the CLI can close by itself.
10
+ */
11
+ class KbConsolidate extends base_1.BaseCommand {
12
+ static args = {
13
+ id: core_1.Args.string({ description: 'Knowledge page id', required: true }),
14
+ };
15
+ static description = 'Mark an accepted page as written to the versioned docs, recording its path';
16
+ static examples = [
17
+ '<%= config.bin %> kb consolidate <page-id> --path troubleshooting/rails.md',
18
+ '<%= config.bin %> kb consolidate <page-id> --path global/git.md --json',
19
+ ];
20
+ static flags = {
21
+ path: core_1.Flags.string({ description: 'Path of the document inside the knowledge-base repo', required: true }),
22
+ };
23
+ async run() {
24
+ const { args, flags } = await this.parse(KbConsolidate);
25
+ const res = await this.api.post(`/cli/v1/knowledge/pages/${args.id}/consolidated`, { source_path: flags.path });
26
+ if (!this.jsonEnabled()) {
27
+ this.log('Knowledge page filed:');
28
+ this.log((0, output_1.renderRecord)(res.data ?? {}));
29
+ }
30
+ return res;
31
+ }
32
+ }
33
+ exports.default = KbConsolidate;
@@ -9,6 +9,8 @@ export default class KbCreate extends BaseCommand {
9
9
  'body-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
10
  'tech-spec': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
11
  'tech-spec-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ 'in-review': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
+ 'review-note': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
14
  project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
15
  };
14
16
  run(): Promise<unknown>;
@@ -10,6 +10,7 @@ class KbCreate extends base_1.BaseCommand {
10
10
  '<%= config.bin %> kb create --project acme-api --title "Kamal rollback" --body "Steps…"',
11
11
  '<%= config.bin %> kb create -p acme-api --title "ADR: queue backend" --kind decision --body-file ./adr.md',
12
12
  '<%= config.bin %> kb create -p acme-api --title "Deploy" --body-file ./body.md --tech-spec-file ./tech.md',
13
+ '<%= config.bin %> kb create -p acme-api --title "Worktree trap" --body-file ./note.md --in-review --review-note "Hit it today, not obvious from the code"',
13
14
  ];
14
15
  static flags = {
15
16
  // Required — passed straight to the server as `project` (key or UUID), resolved there.
@@ -20,6 +21,10 @@ class KbCreate extends base_1.BaseCommand {
20
21
  'body-file': core_1.Flags.string({ description: 'Read the body (markdown) from a local file' }),
21
22
  'tech-spec': core_1.Flags.string({ description: 'Technical section, kept separate from the simple body' }),
22
23
  'tech-spec-file': core_1.Flags.string({ description: 'Read the technical section from a local file' }),
24
+ // Propose instead of publish: the page waits for a human in the review queue and stays out of
25
+ // search, answers and related panels until it is accepted (see `kb list --status in_review`).
26
+ 'in-review': core_1.Flags.boolean({ description: 'Create the page in review instead of publishing it' }),
27
+ 'review-note': core_1.Flags.string({ description: 'One line explaining why the page is worth keeping (review queue)' }),
23
28
  };
24
29
  async run() {
25
30
  const { flags } = await this.parse(KbCreate);
@@ -44,15 +49,20 @@ class KbCreate extends base_1.BaseCommand {
44
49
  this.error(`File not found or unreadable: ${flags['tech-spec-file']}`, { exit: 2 });
45
50
  }
46
51
  }
52
+ if (flags['review-note'] !== undefined && !flags['in-review']) {
53
+ this.error('--review-note only applies to a page created with --in-review.', { exit: 2 });
54
+ }
47
55
  const res = await this.api.post('/cli/v1/knowledge/pages', {
48
56
  project: flags.project,
49
57
  title: flags.title,
50
58
  kind: flags.kind,
51
59
  body,
52
60
  tech_spec: techSpec,
61
+ in_review: flags['in-review'] ? true : undefined,
62
+ review_note: flags['review-note'],
53
63
  });
54
64
  if (!this.jsonEnabled()) {
55
- this.log('Knowledge page created:');
65
+ this.log(flags['in-review'] ? 'Knowledge page proposed, waiting for review:' : 'Knowledge page created:');
56
66
  this.log((0, output_1.renderRecord)(res.data ?? {}));
57
67
  }
58
68
  return res;
@@ -6,6 +6,8 @@ export default class KbList extends BaseCommand {
6
6
  page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
7
7
  project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
8
  kind: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'awaiting-consolidation': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
11
  per: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
12
  };
11
13
  run(): Promise<unknown>;
@@ -9,21 +9,31 @@ class KbList extends base_1.BaseCommand {
9
9
  '<%= config.bin %> kb list',
10
10
  '<%= config.bin %> kb list --project acme-api --kind decision',
11
11
  '<%= config.bin %> kb list --kind note,guide --per 50 --json',
12
+ '<%= config.bin %> kb list --status in_review',
13
+ '<%= config.bin %> kb list --awaiting-consolidation',
12
14
  ];
13
15
  static flags = {
14
16
  // Optional (the endpoint is cross-project): omit it to span every visible project.
15
17
  project: core_1.Flags.string({ char: 'p', description: 'Filter by project (key or UUID)' }),
16
18
  kind: core_1.Flags.string({ description: 'Filter by kind: note|decision|guide (repeatable or comma-separated)', multiple: true }),
19
+ // Without it the server lists published pages only: proposals waiting for review never show up
20
+ // in an ordinary listing.
21
+ status: core_1.Flags.string({ description: 'Filter by review state', options: [...knowledge_1.KNOWLEDGE_STATUSES] }),
22
+ 'awaiting-consolidation': core_1.Flags.boolean({
23
+ description: 'Only accepted pages not yet written to the versioned docs',
24
+ }),
17
25
  per: core_1.Flags.integer({ description: 'Page size' }),
18
26
  ...base_1.pageFlag,
19
27
  };
20
28
  async run() {
21
29
  const { flags } = await this.parse(KbList);
22
30
  const query = (0, knowledge_1.buildPagesQuery)({
31
+ awaitingConsolidation: flags['awaiting-consolidation'],
23
32
  kinds: (0, knowledge_1.normalizeKinds)(flags.kind),
24
33
  page: flags.page,
25
34
  per: flags.per,
26
35
  project: flags.project,
36
+ status: flags.status,
27
37
  });
28
38
  const res = await this.api.get(`/cli/v1/knowledge/pages?${query}`);
29
39
  if (!this.jsonEnabled()) {
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class TicketsLeaseClaim extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ ttl: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'run-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const lease_1 = require("../../../lib/lease");
6
+ const output_1 = require("../../../lib/output");
7
+ class TicketsLeaseClaim extends base_1.BaseCommand {
8
+ static description = 'Claim the working lease on a ticket, so nobody else (person or agent) starts it at the same time';
9
+ static examples = [
10
+ '<%= config.bin %> tickets lease claim <ticket-id> --project acme-api',
11
+ '<%= config.bin %> tickets lease claim DRFL-3 -p driverone-flutter --ttl 3600 --run-id run-42',
12
+ ];
13
+ static args = {
14
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
15
+ };
16
+ static flags = {
17
+ ...base_1.projectFlag,
18
+ ttl: core_1.Flags.integer({ description: 'Lease duration in seconds (server default: 8h; range 1s..30 days)' }),
19
+ 'run-id': core_1.Flags.string({ description: 'Automation run id, for tokens that juggle several concurrent runs' }),
20
+ };
21
+ async run() {
22
+ const { args, flags } = await this.parse(TicketsLeaseClaim);
23
+ const projectId = await this.resolveProjectId(flags.project);
24
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/lease`;
25
+ const body = {};
26
+ if (flags.ttl !== undefined)
27
+ body.ttl_seconds = flags.ttl;
28
+ if (flags['run-id'] !== undefined)
29
+ body.run_id = flags['run-id'];
30
+ let res;
31
+ try {
32
+ res = await this.api.post(path, body);
33
+ }
34
+ catch (error) {
35
+ // 409 = somebody else already holds it. --json prints the full backend envelope (details
36
+ // included) itself and returns, so BaseCommand.catch never trims it down to {code,message};
37
+ // human mode gets the holder spelled out in the message instead (requisito CYCL-14).
38
+ if (this.jsonEnabled()) {
39
+ const envelope = (0, lease_1.leaseErrorEnvelope)(error);
40
+ if (!envelope)
41
+ throw error;
42
+ this.logJson(envelope);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ throw (0, lease_1.describeLeaseConflict)(error);
47
+ }
48
+ if (!this.jsonEnabled()) {
49
+ const lease = res.data ?? {};
50
+ this.log(`Ticket ${args.id} leased until ${lease.expires_at ?? '?'}:`);
51
+ this.log((0, output_1.renderRecord)(leaseSummary(lease)));
52
+ }
53
+ return res;
54
+ }
55
+ }
56
+ exports.default = TicketsLeaseClaim;
57
+ /** Flatten the lease payload for `renderRecord`: `held_by` is a nested object, so print it as one line. */
58
+ function leaseSummary(lease) {
59
+ const heldBy = lease.held_by;
60
+ return {
61
+ ticket: lease.ticket,
62
+ run_id: lease.run_id,
63
+ held_by: heldBy ? `${heldBy.kind} ${heldBy.name ?? heldBy.id}` : null,
64
+ expires_at: lease.expires_at,
65
+ host_id: lease.host_id,
66
+ account_id: lease.account_id,
67
+ agent: lease.agent,
68
+ execution_phase: lease.execution_phase,
69
+ profile_digest: lease.profile_digest,
70
+ };
71
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class TicketsLeaseRelease extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ 'run-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const lease_1 = require("../../../lib/lease");
6
+ const output_1 = require("../../../lib/output");
7
+ class TicketsLeaseRelease extends base_1.BaseCommand {
8
+ static description = 'Release your own working lease on a ticket, so someone else can claim it';
9
+ static examples = [
10
+ '<%= config.bin %> tickets lease release <ticket-id> --project acme-api',
11
+ '<%= config.bin %> tickets lease release DRFL-3 -p driverone-flutter --run-id run-42',
12
+ ];
13
+ static args = {
14
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
15
+ };
16
+ static flags = {
17
+ ...base_1.projectFlag,
18
+ 'run-id': core_1.Flags.string({ description: 'Automation run id, for tokens that juggle several concurrent runs' }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(TicketsLeaseRelease);
22
+ const projectId = await this.resolveProjectId(flags.project);
23
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/lease`;
24
+ const body = {};
25
+ if (flags['run-id'] !== undefined)
26
+ body.run_id = flags['run-id'];
27
+ let res;
28
+ try {
29
+ res = await this.api.delete(path, { body });
30
+ }
31
+ catch (error) {
32
+ // 409 = somebody else already holds it, 404 = no lease to release. --json prints the full
33
+ // backend envelope (details included) itself and returns, so BaseCommand.catch never trims
34
+ // it down to {code,message}; human mode gets the holder spelled out instead (CYCL-14).
35
+ if (this.jsonEnabled()) {
36
+ const envelope = (0, lease_1.leaseErrorEnvelope)(error);
37
+ if (!envelope)
38
+ throw error;
39
+ this.logJson(envelope);
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ throw (0, lease_1.describeLeaseConflict)(error);
44
+ }
45
+ if (!this.jsonEnabled()) {
46
+ this.log(`Ticket ${args.id} lease released:`);
47
+ this.log((0, output_1.renderRecord)(res.data ?? {}));
48
+ }
49
+ return res;
50
+ }
51
+ }
52
+ exports.default = TicketsLeaseRelease;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class TicketsLeaseRenew extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ ttl: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'run-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const lease_1 = require("../../../lib/lease");
6
+ const output_1 = require("../../../lib/output");
7
+ class TicketsLeaseRenew extends base_1.BaseCommand {
8
+ static description = 'Extend your own working lease on a ticket before it expires';
9
+ static examples = [
10
+ '<%= config.bin %> tickets lease renew <ticket-id> --project acme-api',
11
+ '<%= config.bin %> tickets lease renew DRFL-3 -p driverone-flutter --ttl 3600 --run-id run-42',
12
+ ];
13
+ static args = {
14
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
15
+ };
16
+ static flags = {
17
+ ...base_1.projectFlag,
18
+ ttl: core_1.Flags.integer({ description: 'New lease duration in seconds (server default: 8h; range 1s..30 days)' }),
19
+ 'run-id': core_1.Flags.string({ description: 'Automation run id, for tokens that juggle several concurrent runs' }),
20
+ };
21
+ async run() {
22
+ const { args, flags } = await this.parse(TicketsLeaseRenew);
23
+ const projectId = await this.resolveProjectId(flags.project);
24
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/lease`;
25
+ const body = {};
26
+ if (flags.ttl !== undefined)
27
+ body.ttl_seconds = flags.ttl;
28
+ if (flags['run-id'] !== undefined)
29
+ body.run_id = flags['run-id'];
30
+ let res;
31
+ try {
32
+ res = await this.api.put(path, body);
33
+ }
34
+ catch (error) {
35
+ // 409 = somebody else already holds it, 404 = no lease to renew. --json prints the full
36
+ // backend envelope (details included) itself and returns, so BaseCommand.catch never trims
37
+ // it down to {code,message}; human mode gets the holder spelled out instead (CYCL-14).
38
+ if (this.jsonEnabled()) {
39
+ const envelope = (0, lease_1.leaseErrorEnvelope)(error);
40
+ if (!envelope)
41
+ throw error;
42
+ this.logJson(envelope);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ throw (0, lease_1.describeLeaseConflict)(error);
47
+ }
48
+ if (!this.jsonEnabled()) {
49
+ const lease = res.data ?? {};
50
+ this.log(`Ticket ${args.id} lease renewed until ${lease.expires_at ?? '?'}:`);
51
+ this.log((0, output_1.renderRecord)(leaseSummary(lease)));
52
+ }
53
+ return res;
54
+ }
55
+ }
56
+ exports.default = TicketsLeaseRenew;
57
+ /** Flatten the lease payload for `renderRecord`: `held_by` is a nested object, so print it as one line. */
58
+ function leaseSummary(lease) {
59
+ const heldBy = lease.held_by;
60
+ return {
61
+ ticket: lease.ticket,
62
+ run_id: lease.run_id,
63
+ held_by: heldBy ? `${heldBy.kind} ${heldBy.name ?? heldBy.id}` : null,
64
+ expires_at: lease.expires_at,
65
+ host_id: lease.host_id,
66
+ account_id: lease.account_id,
67
+ agent: lease.agent,
68
+ execution_phase: lease.execution_phase,
69
+ profile_digest: lease.profile_digest,
70
+ };
71
+ }
@@ -7,9 +7,15 @@ export declare const KNOWLEDGE_KINDS: readonly ["note", "decision", "guide"];
7
7
  export declare function normalizeKinds(values?: string[]): string[];
8
8
  /** Collapse whitespace and truncate a cell so the list table stays single-line. */
9
9
  export declare function truncate(value: string, max?: number): string;
10
+ /**
11
+ * Review states a page can be filtered by (Knowledge::Page#status). `all` drops the filter;
12
+ * omitting `--status` leaves the server default, which is `published` — so an ordinary `kb list`
13
+ * never shows proposals waiting for review.
14
+ */
15
+ export declare const KNOWLEDGE_STATUSES: readonly ["published", "in_review", "rejected", "all"];
10
16
  /**
11
17
  * Build the shared query string for `kb list` / `kb search`. Kinds go out as the
12
- * `kind[]` array param; `page` is always sent, `per`/`project`/`q` only when set.
18
+ * `kind[]` array param; `page` is always sent, the rest only when set.
13
19
  */
14
20
  export declare function buildPagesQuery(opts: {
15
21
  kinds: string[];
@@ -18,6 +24,8 @@ export declare function buildPagesQuery(opts: {
18
24
  project?: string;
19
25
  q?: string;
20
26
  related?: boolean;
27
+ status?: string;
28
+ awaitingConsolidation?: boolean;
21
29
  }): string;
22
30
  /** A related-page row as returned by the server (KnowledgeRelatedPageSerializer). */
23
31
  export interface RelatedRow extends Record<string, unknown> {
@@ -40,5 +48,11 @@ export declare function renderRelated(rows: RelatedRow[], heading?: string, inde
40
48
  * order as the table above it. Results without related pages are simply left out.
41
49
  */
42
50
  export declare function renderRelatedGroups(pages: Array<Record<string, unknown>>, grouped: Record<string, RelatedRow[]>): string;
43
- /** Render the shared list/search table: TITLE, KIND, PROJECT, AUTHOR, UPDATED. */
51
+ /**
52
+ * Render the shared list/search table: TITLE, KIND, PROJECT, AUTHOR, UPDATED.
53
+ *
54
+ * STATUS shows up only when a row is not `published` — an ordinary listing already contains
55
+ * nothing but published pages, so the extra column would be noise; the review queue, where the
56
+ * state is the whole point, gets it automatically.
57
+ */
44
58
  export declare function renderPagesTable(pages: Array<Record<string, unknown>>): string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KNOWLEDGE_KINDS = void 0;
3
+ exports.KNOWLEDGE_STATUSES = exports.KNOWLEDGE_KINDS = void 0;
4
4
  exports.normalizeKinds = normalizeKinds;
5
5
  exports.truncate = truncate;
6
6
  exports.buildPagesQuery = buildPagesQuery;
@@ -27,9 +27,15 @@ function truncate(value, max = 60) {
27
27
  const flat = value.replace(/\s+/g, ' ').trim();
28
28
  return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
29
29
  }
30
+ /**
31
+ * Review states a page can be filtered by (Knowledge::Page#status). `all` drops the filter;
32
+ * omitting `--status` leaves the server default, which is `published` — so an ordinary `kb list`
33
+ * never shows proposals waiting for review.
34
+ */
35
+ exports.KNOWLEDGE_STATUSES = ['published', 'in_review', 'rejected', 'all'];
30
36
  /**
31
37
  * Build the shared query string for `kb list` / `kb search`. Kinds go out as the
32
- * `kind[]` array param; `page` is always sent, `per`/`project`/`q` only when set.
38
+ * `kind[]` array param; `page` is always sent, the rest only when set.
33
39
  */
34
40
  function buildPagesQuery(opts) {
35
41
  const query = new URLSearchParams();
@@ -44,6 +50,10 @@ function buildPagesQuery(opts) {
44
50
  query.set('q', opts.q);
45
51
  if (opts.related)
46
52
  query.set('related', '1');
53
+ if (opts.status)
54
+ query.set('status', opts.status);
55
+ if (opts.awaitingConsolidation)
56
+ query.set('awaiting_consolidation', '1');
47
57
  return query.toString();
48
58
  }
49
59
  /**
@@ -72,13 +82,23 @@ function renderRelatedGroups(pages, grouped) {
72
82
  .map(({ rows, title }) => renderRelated(rows, ` ${(0, output_1.sanitize)(title)}`, ' '));
73
83
  return blocks.length === 0 ? '' : ['\n🔗 Related:', ...blocks].join('\n');
74
84
  }
75
- /** Render the shared list/search table: TITLE, KIND, PROJECT, AUTHOR, UPDATED. */
85
+ /**
86
+ * Render the shared list/search table: TITLE, KIND, PROJECT, AUTHOR, UPDATED.
87
+ *
88
+ * STATUS shows up only when a row is not `published` — an ordinary listing already contains
89
+ * nothing but published pages, so the extra column would be noise; the review queue, where the
90
+ * state is the whole point, gets it automatically.
91
+ */
76
92
  function renderPagesTable(pages) {
77
- return (0, output_1.renderTable)(['TITLE', 'KIND', 'PROJECT', 'AUTHOR', 'UPDATED'], pages.map((page) => [
78
- truncate(String(page.title ?? '')),
79
- String(page.kind ?? ''),
80
- String(page.project ?? ''),
81
- String(page.author ?? ''),
82
- String(page.updated_at ?? ''),
83
- ]));
93
+ const showStatus = pages.some((page) => page.status !== undefined && page.status !== 'published');
94
+ const headers = showStatus
95
+ ? ['TITLE', 'KIND', 'STATUS', 'PROJECT', 'AUTHOR', 'UPDATED']
96
+ : ['TITLE', 'KIND', 'PROJECT', 'AUTHOR', 'UPDATED'];
97
+ return (0, output_1.renderTable)(headers, pages.map((page) => {
98
+ const row = [truncate(String(page.title ?? '')), String(page.kind ?? '')];
99
+ if (showStatus)
100
+ row.push(String(page.status ?? ''));
101
+ row.push(String(page.project ?? ''), String(page.author ?? ''), String(page.updated_at ?? ''));
102
+ return row;
103
+ }));
84
104
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * On a 409 lease conflict, rewrite the error message to name who holds the ticket and until when.
3
+ * An automation reading "R409-LEASE-001: Ticket is already leased" alone cannot decide whether to
4
+ * skip to the next ticket — it needs the holder. Every other status/code (and a 409 without the
5
+ * expected `details.holder` shape) passes through untouched, so unrelated errors keep the server's
6
+ * own wording. Human-mode only: `--json` prints its own full envelope instead (see
7
+ * `leaseErrorEnvelope`), so it never goes through this rewrite.
8
+ */
9
+ export declare function describeLeaseConflict(error: unknown): unknown;
10
+ /** Allow-listed `details` for the --json envelope: only the fields a lease conflict is documented to carry. */
11
+ interface LeaseErrorDetails {
12
+ holder: {
13
+ held_by: {
14
+ kind: string;
15
+ id: string;
16
+ name?: string;
17
+ };
18
+ expires_at?: string;
19
+ };
20
+ }
21
+ /**
22
+ * Full `{error:{code,message,details}}` envelope for a lease command failure in `--json` mode.
23
+ * `BaseCommand.catch` — shared by every command in the CLI — only ever emits `{code,message}`; a
24
+ * script reading `--json` on a 409 needs `details.holder` too (who holds the ticket, until when),
25
+ * so lease commands print this themselves and return before the error reaches `catch()`, instead of
26
+ * changing that shared, cross-cutting behaviour (out of scope for CYCL-14).
27
+ *
28
+ * `details` is rebuilt field-by-field from an explicit allow-list (`holder.held_by.{kind,id,name}`,
29
+ * `holder.expires_at`) instead of forwarding `error.details` verbatim — same reasoning as
30
+ * `BaseCommand.catch`'s own allow-listed envelope (see `base.ts`): never serialize a server-shaped
31
+ * object of arbitrary provenance straight into `--json` output. Any other field the backend happens
32
+ * to send inside `details` (e.g. a stray `holder.ticket`) is dropped, not forwarded.
33
+ *
34
+ * Returns undefined for anything that isn't an `ApiRequestError`: callers should let those propagate
35
+ * to `catch()` as usual.
36
+ */
37
+ export declare function leaseErrorEnvelope(error: unknown): {
38
+ error: {
39
+ code: string;
40
+ message: string;
41
+ details?: LeaseErrorDetails;
42
+ };
43
+ } | undefined;
44
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeLeaseConflict = describeLeaseConflict;
4
+ exports.leaseErrorEnvelope = leaseErrorEnvelope;
5
+ const api_1 = require("./api");
6
+ /** Extract `{kind,id,name?,expiresAt?}` from a 409's `details`, or undefined if the shape doesn't match. */
7
+ function parseLeaseHolder(details) {
8
+ const holder = details?.holder;
9
+ const heldBy = holder?.held_by;
10
+ if (!heldBy?.kind || !heldBy.id)
11
+ return undefined;
12
+ return { kind: heldBy.kind, id: heldBy.id, name: heldBy.name, expiresAt: holder?.expires_at };
13
+ }
14
+ /**
15
+ * On a 409 lease conflict, rewrite the error message to name who holds the ticket and until when.
16
+ * An automation reading "R409-LEASE-001: Ticket is already leased" alone cannot decide whether to
17
+ * skip to the next ticket — it needs the holder. Every other status/code (and a 409 without the
18
+ * expected `details.holder` shape) passes through untouched, so unrelated errors keep the server's
19
+ * own wording. Human-mode only: `--json` prints its own full envelope instead (see
20
+ * `leaseErrorEnvelope`), so it never goes through this rewrite.
21
+ */
22
+ function describeLeaseConflict(error) {
23
+ if (!(error instanceof api_1.ApiRequestError) || error.status !== 409)
24
+ return error;
25
+ const holder = parseLeaseHolder(error.details);
26
+ if (!holder)
27
+ return error;
28
+ const who = holder.name ? `${holder.kind} "${holder.name}"` : `${holder.kind} ${holder.id}`;
29
+ const until = holder.expiresAt ? ` until ${holder.expiresAt}` : '';
30
+ return new api_1.ApiRequestError(error.status, error.code, `Ticket already leased by ${who}${until}.`, error.details);
31
+ }
32
+ /**
33
+ * Full `{error:{code,message,details}}` envelope for a lease command failure in `--json` mode.
34
+ * `BaseCommand.catch` — shared by every command in the CLI — only ever emits `{code,message}`; a
35
+ * script reading `--json` on a 409 needs `details.holder` too (who holds the ticket, until when),
36
+ * so lease commands print this themselves and return before the error reaches `catch()`, instead of
37
+ * changing that shared, cross-cutting behaviour (out of scope for CYCL-14).
38
+ *
39
+ * `details` is rebuilt field-by-field from an explicit allow-list (`holder.held_by.{kind,id,name}`,
40
+ * `holder.expires_at`) instead of forwarding `error.details` verbatim — same reasoning as
41
+ * `BaseCommand.catch`'s own allow-listed envelope (see `base.ts`): never serialize a server-shaped
42
+ * object of arbitrary provenance straight into `--json` output. Any other field the backend happens
43
+ * to send inside `details` (e.g. a stray `holder.ticket`) is dropped, not forwarded.
44
+ *
45
+ * Returns undefined for anything that isn't an `ApiRequestError`: callers should let those propagate
46
+ * to `catch()` as usual.
47
+ */
48
+ function leaseErrorEnvelope(error) {
49
+ if (!(error instanceof api_1.ApiRequestError))
50
+ return undefined;
51
+ const holder = parseLeaseHolder(error.details);
52
+ const details = holder
53
+ ? {
54
+ holder: {
55
+ held_by: holder.name ? { kind: holder.kind, id: holder.id, name: holder.name } : { kind: holder.kind, id: holder.id },
56
+ ...(holder.expiresAt ? { expires_at: holder.expiresAt } : {}),
57
+ },
58
+ }
59
+ : undefined;
60
+ return { error: { code: error.code, message: error.message, details } };
61
+ }