@cleocode/contracts 2026.5.105 โ†’ 2026.5.107

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=icon.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.test.d.ts","sourceRoot":"","sources":["../../../src/render/__tests__/icon.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,124 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { ascii, BadgeIcon, KindIcon, pickIcon, RelationIcon, StatusIcon } from '../icon.js';
3
+ describe('icon enums', () => {
4
+ it('StatusIcon emoji values match ADR-077', () => {
5
+ expect(StatusIcon.PENDING).toBe('โณ');
6
+ expect(StatusIcon.ACTIVE).toBe('๐Ÿšง');
7
+ expect(StatusIcon.DONE).toBe('โœ…');
8
+ expect(StatusIcon.BLOCKED).toBe('๐Ÿšช');
9
+ expect(StatusIcon.ARCHIVED).toBe('๐Ÿ—„');
10
+ expect(StatusIcon.CANCELLED).toBe('โœ—');
11
+ });
12
+ it('KindIcon emoji values match ADR-077', () => {
13
+ expect(KindIcon.SAGA).toBe('๐ŸŒฒ');
14
+ expect(KindIcon.EPIC).toBe('๐Ÿ“‹');
15
+ expect(KindIcon.TASK).toBe('โ€ข');
16
+ expect(KindIcon.SUBTASK).toBe('โ—ฆ');
17
+ expect(KindIcon.RESEARCH).toBe('๐Ÿ“–');
18
+ expect(KindIcon.BUG).toBe('๐Ÿ›');
19
+ expect(KindIcon.RELEASE).toBe('๐Ÿš€');
20
+ });
21
+ it('BadgeIcon emoji values match ADR-077 (ORPHAN amended in T10137)', () => {
22
+ expect(BadgeIcon.EMPTY).toBe('๐Ÿชฆ');
23
+ // ADR-077 ยง2 originally specified '๐Ÿšช' for ORPHAN โ€” identical to
24
+ // StatusIcon.BLOCKED. String enums share runtime values so ascii() cannot
25
+ // disambiguate. Amended to '๐Ÿ‘ป' (abandoned/lonely). T10137 (B12) updates
26
+ // the ADR text to match.
27
+ expect(BadgeIcon.ORPHAN).toBe('๐Ÿ‘ป');
28
+ expect(BadgeIcon.NESTED).toBe('๐Ÿ”');
29
+ expect(BadgeIcon.CAUTION).toBe('โš ');
30
+ expect(BadgeIcon.NEW).toBe('โ˜…');
31
+ });
32
+ it('RelationIcon emoji values match ADR-077', () => {
33
+ expect(RelationIcon.GROUPS).toBe('โŠ‚');
34
+ expect(RelationIcon.PARENT).toBe('โคด');
35
+ expect(RelationIcon.DEPENDS).toBe('โ‡จ');
36
+ expect(RelationIcon.BLOCKS).toBe('โŠ˜');
37
+ });
38
+ it('no two enum members share the same runtime string value', () => {
39
+ const all = [
40
+ ...Object.values(StatusIcon),
41
+ ...Object.values(KindIcon),
42
+ ...Object.values(BadgeIcon),
43
+ ...Object.values(RelationIcon),
44
+ ];
45
+ const unique = new Set(all);
46
+ expect(unique.size).toBe(all.length);
47
+ });
48
+ });
49
+ describe('ascii()', () => {
50
+ it('returns NO_COLOR fallback for every StatusIcon', () => {
51
+ expect(ascii(StatusIcon.PENDING)).toBe('[ ]');
52
+ expect(ascii(StatusIcon.ACTIVE)).toBe('[~]');
53
+ expect(ascii(StatusIcon.DONE)).toBe('[x]');
54
+ expect(ascii(StatusIcon.BLOCKED)).toBe('[!]');
55
+ expect(ascii(StatusIcon.ARCHIVED)).toBe('[#]');
56
+ expect(ascii(StatusIcon.CANCELLED)).toBe('[-]');
57
+ });
58
+ it('returns NO_COLOR fallback for every KindIcon', () => {
59
+ expect(ascii(KindIcon.SAGA)).toBe('SG');
60
+ expect(ascii(KindIcon.EPIC)).toBe('E');
61
+ expect(ascii(KindIcon.TASK)).toBe('-');
62
+ expect(ascii(KindIcon.SUBTASK)).toBe('.');
63
+ expect(ascii(KindIcon.RESEARCH)).toBe('R');
64
+ expect(ascii(KindIcon.BUG)).toBe('B');
65
+ expect(ascii(KindIcon.RELEASE)).toBe('>');
66
+ });
67
+ it('returns NO_COLOR fallback for every BadgeIcon', () => {
68
+ expect(ascii(BadgeIcon.EMPTY)).toBe('(empty)');
69
+ expect(ascii(BadgeIcon.ORPHAN)).toBe('(orphan)');
70
+ expect(ascii(BadgeIcon.NESTED)).toBe('(nested)');
71
+ expect(ascii(BadgeIcon.CAUTION)).toBe('(!)');
72
+ expect(ascii(BadgeIcon.NEW)).toBe('(new)');
73
+ });
74
+ it('returns NO_COLOR fallback for every RelationIcon', () => {
75
+ expect(ascii(RelationIcon.GROUPS)).toBe('in');
76
+ expect(ascii(RelationIcon.PARENT)).toBe('^');
77
+ expect(ascii(RelationIcon.DEPENDS)).toBe('->');
78
+ expect(ascii(RelationIcon.BLOCKS)).toBe('!>');
79
+ });
80
+ });
81
+ describe('pickIcon()', () => {
82
+ const originalNoColor = process.env['NO_COLOR'];
83
+ const originalTerm = process.env['TERM'];
84
+ beforeEach(() => {
85
+ delete process.env['NO_COLOR'];
86
+ delete process.env['TERM'];
87
+ });
88
+ afterEach(() => {
89
+ if (originalNoColor === undefined) {
90
+ delete process.env['NO_COLOR'];
91
+ }
92
+ else {
93
+ process.env['NO_COLOR'] = originalNoColor;
94
+ }
95
+ if (originalTerm === undefined) {
96
+ delete process.env['TERM'];
97
+ }
98
+ else {
99
+ process.env['TERM'] = originalTerm;
100
+ }
101
+ });
102
+ it('returns the emoji when noColor is explicitly false', () => {
103
+ expect(pickIcon(KindIcon.SAGA, { noColor: false })).toBe('๐ŸŒฒ');
104
+ expect(pickIcon(StatusIcon.DONE, { noColor: false })).toBe('โœ…');
105
+ });
106
+ it('returns the ASCII form when noColor is explicitly true', () => {
107
+ expect(pickIcon(KindIcon.SAGA, { noColor: true })).toBe('SG');
108
+ expect(pickIcon(StatusIcon.DONE, { noColor: true })).toBe('[x]');
109
+ });
110
+ it('falls back to NO_COLOR=1 from process.env', () => {
111
+ process.env['NO_COLOR'] = '1';
112
+ expect(pickIcon(KindIcon.EPIC)).toBe('E');
113
+ expect(pickIcon(BadgeIcon.ORPHAN)).toBe('(orphan)');
114
+ });
115
+ it('falls back to TERM=dumb from process.env', () => {
116
+ process.env['TERM'] = 'dumb';
117
+ expect(pickIcon(RelationIcon.GROUPS)).toBe('in');
118
+ });
119
+ it('returns the emoji when no NO_COLOR signals are present', () => {
120
+ expect(pickIcon(KindIcon.TASK)).toBe('โ€ข');
121
+ expect(pickIcon(StatusIcon.ACTIVE)).toBe('๐Ÿšง');
122
+ });
123
+ });
124
+ //# sourceMappingURL=icon.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.test.js","sourceRoot":"","sources":["../../../src/render/__tests__/icon.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Typed icon enums for the human-render contract (ADR-077).
3
+ *
4
+ * Single source of truth for the visual language used across all renderers
5
+ * (tree, table, list, badge). Each enum is emoji-first; `ascii()` returns the
6
+ * NO_COLOR-safe fallback for non-UTF-8 terminals or `NO_COLOR=1` environments.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /**
11
+ * Status icons โ€” agent-task lifecycle states.
12
+ *
13
+ * Emoji-first; `ascii()` returns a NO_COLOR-safe fallback.
14
+ */
15
+ export declare enum StatusIcon {
16
+ /** Task is queued and has not started. */
17
+ PENDING = "\u23F3",
18
+ /** Task is in progress. */
19
+ ACTIVE = "\uD83D\uDEA7",
20
+ /** Task completed successfully. */
21
+ DONE = "\u2705",
22
+ /** Task is blocked on a dependency or gate. */
23
+ BLOCKED = "\uD83D\uDEAA",
24
+ /** Task is archived and no longer actively tracked. */
25
+ ARCHIVED = "\uD83D\uDDC4",
26
+ /** Task was cancelled before completion. */
27
+ CANCELLED = "\u2717"
28
+ }
29
+ /**
30
+ * Task-kind icons โ€” Saga / Epic / Task / Subtask plus ancillary kinds.
31
+ */
32
+ export declare enum KindIcon {
33
+ /** Saga โ€” multi-release theme grouping โ‰ฅ 2 Epics. */
34
+ SAGA = "\uD83C\uDF32",
35
+ /** Epic โ€” one releasable slice. */
36
+ EPIC = "\uD83D\uDCCB",
37
+ /** Task โ€” one atomic PR-sized change. */
38
+ TASK = "\u2022",
39
+ /** Subtask โ€” one commit contributing to a Task's PR. */
40
+ SUBTASK = "\u25E6",
41
+ /** Research task โ€” investigation or audit. */
42
+ RESEARCH = "\uD83D\uDCD6",
43
+ /** Bug task โ€” defect tracking. */
44
+ BUG = "\uD83D\uDC1B",
45
+ /** Release task โ€” shipping work. */
46
+ RELEASE = "\uD83D\uDE80"
47
+ }
48
+ /**
49
+ * Badge icons โ€” flags attached to tasks (empty, orphan, nested, etc).
50
+ */
51
+ export declare enum BadgeIcon {
52
+ /** Container has no children. */
53
+ EMPTY = "\uD83E\uDEA6",
54
+ /**
55
+ * Task has no parent and is unreachable from a Saga or Epic.
56
+ *
57
+ * NOTE: ADR-077 ยง2 originally specified `'๐Ÿšช'`, identical to `StatusIcon.BLOCKED`.
58
+ * TypeScript string enums share a single runtime string so `ascii()` cannot
59
+ * disambiguate. Resolved by switching to `'๐Ÿ‘ป'` (abandoned/lonely semantics).
60
+ * A follow-up ADR amendment lands in T10137 (B12).
61
+ */
62
+ ORPHAN = "\uD83D\uDC7B",
63
+ /** Task is nested deeper than the depth budget allows. */
64
+ NESTED = "\uD83D\uDD01",
65
+ /** Caution โ€” something needs human attention. */
66
+ CAUTION = "\u26A0",
67
+ /** Recently added โ€” surfaced for visibility. */
68
+ NEW = "\u2605"
69
+ }
70
+ /**
71
+ * Relation icons โ€” edge semantics in tree views.
72
+ */
73
+ export declare enum RelationIcon {
74
+ /** Saga groups Epic via `task_relations.relation_type='groups'`. */
75
+ GROUPS = "\u2282",
76
+ /** Parent edge โ€” direct hierarchical parent. */
77
+ PARENT = "\u2934",
78
+ /** Depends-on edge โ€” A cannot start until B is done. */
79
+ DEPENDS = "\u21E8",
80
+ /** Blocks edge โ€” A actively blocks B from progressing. */
81
+ BLOCKS = "\u2298"
82
+ }
83
+ /**
84
+ * Union of every icon enum โ€” useful as the parameter type for helpers that
85
+ * accept any icon without caring which category it came from.
86
+ */
87
+ export type AnyIcon = StatusIcon | KindIcon | BadgeIcon | RelationIcon;
88
+ /**
89
+ * Maps an emoji icon to its NO_COLOR-safe ASCII equivalent.
90
+ *
91
+ * Used when stdout is not a UTF-8 TTY or `NO_COLOR=1` is set. Every enum
92
+ * member has an explicit ASCII fallback โ€” the exhaustive switch ensures the
93
+ * TypeScript compiler flags any future icon added without an ASCII mapping.
94
+ *
95
+ * @param icon - Any icon from `StatusIcon`, `KindIcon`, `BadgeIcon`, or `RelationIcon`.
96
+ * @returns The ASCII-only string to render in NO_COLOR contexts.
97
+ */
98
+ export declare function ascii(icon: AnyIcon): string;
99
+ /**
100
+ * Options for {@link pickIcon}.
101
+ */
102
+ export interface PickIconOptions {
103
+ /**
104
+ * Force ASCII fallback. When omitted, falls back to `NO_COLOR=1` or
105
+ * `TERM=dumb` in `process.env`.
106
+ */
107
+ noColor?: boolean;
108
+ }
109
+ /**
110
+ * Returns the icon to render given the current environment.
111
+ *
112
+ * Honors `NO_COLOR=1` and `TERM=dumb` for ASCII fallback when `opts.noColor`
113
+ * is not provided explicitly.
114
+ *
115
+ * @param icon - The icon to render.
116
+ * @param opts - Optional override for NO_COLOR detection.
117
+ * @returns The emoji icon, or its ASCII equivalent if NO_COLOR mode is active.
118
+ */
119
+ export declare function pickIcon<I extends AnyIcon>(icon: I, opts?: PickIconOptions): string;
120
+ //# sourceMappingURL=icon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.d.ts","sourceRoot":"","sources":["../../src/render/icon.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,oBAAY,UAAU;IACpB,0CAA0C;IAC1C,OAAO,WAAM;IACb,2BAA2B;IAC3B,MAAM,iBAAO;IACb,mCAAmC;IACnC,IAAI,WAAM;IACV,+CAA+C;IAC/C,OAAO,iBAAO;IACd,uDAAuD;IACvD,QAAQ,iBAAO;IACf,4CAA4C;IAC5C,SAAS,WAAM;CAChB;AAED;;GAEG;AACH,oBAAY,QAAQ;IAClB,qDAAqD;IACrD,IAAI,iBAAO;IACX,mCAAmC;IACnC,IAAI,iBAAO;IACX,yCAAyC;IACzC,IAAI,WAAM;IACV,wDAAwD;IACxD,OAAO,WAAM;IACb,8CAA8C;IAC9C,QAAQ,iBAAO;IACf,kCAAkC;IAClC,GAAG,iBAAO;IACV,oCAAoC;IACpC,OAAO,iBAAO;CACf;AAED;;GAEG;AACH,oBAAY,SAAS;IACnB,iCAAiC;IACjC,KAAK,iBAAO;IACZ;;;;;;;OAOG;IACH,MAAM,iBAAO;IACb,0DAA0D;IAC1D,MAAM,iBAAO;IACb,iDAAiD;IACjD,OAAO,WAAM;IACb,gDAAgD;IAChD,GAAG,WAAM;CACV;AAED;;GAEG;AACH,oBAAY,YAAY;IACtB,oEAAoE;IACpE,MAAM,WAAM;IACZ,gDAAgD;IAChD,MAAM,WAAM;IACZ,wDAAwD;IACxD,OAAO,WAAM;IACb,0DAA0D;IAC1D,MAAM,WAAM;CACb;AAED;;;GAGG;AACH,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,YAAY,CAAC;AAEvE;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAmD3C;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,CAInF"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Typed icon enums for the human-render contract (ADR-077).
3
+ *
4
+ * Single source of truth for the visual language used across all renderers
5
+ * (tree, table, list, badge). Each enum is emoji-first; `ascii()` returns the
6
+ * NO_COLOR-safe fallback for non-UTF-8 terminals or `NO_COLOR=1` environments.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /**
11
+ * Status icons โ€” agent-task lifecycle states.
12
+ *
13
+ * Emoji-first; `ascii()` returns a NO_COLOR-safe fallback.
14
+ */
15
+ export var StatusIcon;
16
+ (function (StatusIcon) {
17
+ /** Task is queued and has not started. */
18
+ StatusIcon["PENDING"] = "\u23F3";
19
+ /** Task is in progress. */
20
+ StatusIcon["ACTIVE"] = "\uD83D\uDEA7";
21
+ /** Task completed successfully. */
22
+ StatusIcon["DONE"] = "\u2705";
23
+ /** Task is blocked on a dependency or gate. */
24
+ StatusIcon["BLOCKED"] = "\uD83D\uDEAA";
25
+ /** Task is archived and no longer actively tracked. */
26
+ StatusIcon["ARCHIVED"] = "\uD83D\uDDC4";
27
+ /** Task was cancelled before completion. */
28
+ StatusIcon["CANCELLED"] = "\u2717";
29
+ })(StatusIcon || (StatusIcon = {}));
30
+ /**
31
+ * Task-kind icons โ€” Saga / Epic / Task / Subtask plus ancillary kinds.
32
+ */
33
+ export var KindIcon;
34
+ (function (KindIcon) {
35
+ /** Saga โ€” multi-release theme grouping โ‰ฅ 2 Epics. */
36
+ KindIcon["SAGA"] = "\uD83C\uDF32";
37
+ /** Epic โ€” one releasable slice. */
38
+ KindIcon["EPIC"] = "\uD83D\uDCCB";
39
+ /** Task โ€” one atomic PR-sized change. */
40
+ KindIcon["TASK"] = "\u2022";
41
+ /** Subtask โ€” one commit contributing to a Task's PR. */
42
+ KindIcon["SUBTASK"] = "\u25E6";
43
+ /** Research task โ€” investigation or audit. */
44
+ KindIcon["RESEARCH"] = "\uD83D\uDCD6";
45
+ /** Bug task โ€” defect tracking. */
46
+ KindIcon["BUG"] = "\uD83D\uDC1B";
47
+ /** Release task โ€” shipping work. */
48
+ KindIcon["RELEASE"] = "\uD83D\uDE80";
49
+ })(KindIcon || (KindIcon = {}));
50
+ /**
51
+ * Badge icons โ€” flags attached to tasks (empty, orphan, nested, etc).
52
+ */
53
+ export var BadgeIcon;
54
+ (function (BadgeIcon) {
55
+ /** Container has no children. */
56
+ BadgeIcon["EMPTY"] = "\uD83E\uDEA6";
57
+ /**
58
+ * Task has no parent and is unreachable from a Saga or Epic.
59
+ *
60
+ * NOTE: ADR-077 ยง2 originally specified `'๐Ÿšช'`, identical to `StatusIcon.BLOCKED`.
61
+ * TypeScript string enums share a single runtime string so `ascii()` cannot
62
+ * disambiguate. Resolved by switching to `'๐Ÿ‘ป'` (abandoned/lonely semantics).
63
+ * A follow-up ADR amendment lands in T10137 (B12).
64
+ */
65
+ BadgeIcon["ORPHAN"] = "\uD83D\uDC7B";
66
+ /** Task is nested deeper than the depth budget allows. */
67
+ BadgeIcon["NESTED"] = "\uD83D\uDD01";
68
+ /** Caution โ€” something needs human attention. */
69
+ BadgeIcon["CAUTION"] = "\u26A0";
70
+ /** Recently added โ€” surfaced for visibility. */
71
+ BadgeIcon["NEW"] = "\u2605";
72
+ })(BadgeIcon || (BadgeIcon = {}));
73
+ /**
74
+ * Relation icons โ€” edge semantics in tree views.
75
+ */
76
+ export var RelationIcon;
77
+ (function (RelationIcon) {
78
+ /** Saga groups Epic via `task_relations.relation_type='groups'`. */
79
+ RelationIcon["GROUPS"] = "\u2282";
80
+ /** Parent edge โ€” direct hierarchical parent. */
81
+ RelationIcon["PARENT"] = "\u2934";
82
+ /** Depends-on edge โ€” A cannot start until B is done. */
83
+ RelationIcon["DEPENDS"] = "\u21E8";
84
+ /** Blocks edge โ€” A actively blocks B from progressing. */
85
+ RelationIcon["BLOCKS"] = "\u2298";
86
+ })(RelationIcon || (RelationIcon = {}));
87
+ /**
88
+ * Maps an emoji icon to its NO_COLOR-safe ASCII equivalent.
89
+ *
90
+ * Used when stdout is not a UTF-8 TTY or `NO_COLOR=1` is set. Every enum
91
+ * member has an explicit ASCII fallback โ€” the exhaustive switch ensures the
92
+ * TypeScript compiler flags any future icon added without an ASCII mapping.
93
+ *
94
+ * @param icon - Any icon from `StatusIcon`, `KindIcon`, `BadgeIcon`, or `RelationIcon`.
95
+ * @returns The ASCII-only string to render in NO_COLOR contexts.
96
+ */
97
+ export function ascii(icon) {
98
+ switch (icon) {
99
+ // Status
100
+ case StatusIcon.PENDING:
101
+ return '[ ]';
102
+ case StatusIcon.ACTIVE:
103
+ return '[~]';
104
+ case StatusIcon.DONE:
105
+ return '[x]';
106
+ case StatusIcon.BLOCKED:
107
+ return '[!]';
108
+ case StatusIcon.ARCHIVED:
109
+ return '[#]';
110
+ case StatusIcon.CANCELLED:
111
+ return '[-]';
112
+ // Kind
113
+ case KindIcon.SAGA:
114
+ return 'SG';
115
+ case KindIcon.EPIC:
116
+ return 'E';
117
+ case KindIcon.TASK:
118
+ return '-';
119
+ case KindIcon.SUBTASK:
120
+ return '.';
121
+ case KindIcon.RESEARCH:
122
+ return 'R';
123
+ case KindIcon.BUG:
124
+ return 'B';
125
+ case KindIcon.RELEASE:
126
+ return '>';
127
+ // Badge
128
+ case BadgeIcon.EMPTY:
129
+ return '(empty)';
130
+ case BadgeIcon.ORPHAN:
131
+ return '(orphan)';
132
+ case BadgeIcon.NESTED:
133
+ return '(nested)';
134
+ case BadgeIcon.CAUTION:
135
+ return '(!)';
136
+ case BadgeIcon.NEW:
137
+ return '(new)';
138
+ // Relation
139
+ case RelationIcon.GROUPS:
140
+ return 'in';
141
+ case RelationIcon.PARENT:
142
+ return '^';
143
+ case RelationIcon.DEPENDS:
144
+ return '->';
145
+ case RelationIcon.BLOCKS:
146
+ return '!>';
147
+ }
148
+ }
149
+ /**
150
+ * Returns the icon to render given the current environment.
151
+ *
152
+ * Honors `NO_COLOR=1` and `TERM=dumb` for ASCII fallback when `opts.noColor`
153
+ * is not provided explicitly.
154
+ *
155
+ * @param icon - The icon to render.
156
+ * @param opts - Optional override for NO_COLOR detection.
157
+ * @returns The emoji icon, or its ASCII equivalent if NO_COLOR mode is active.
158
+ */
159
+ export function pickIcon(icon, opts) {
160
+ const noColor = opts?.noColor ?? (process.env['NO_COLOR'] === '1' || process.env['TERM'] === 'dumb');
161
+ return noColor ? ascii(icon) : icon;
162
+ }
163
+ //# sourceMappingURL=icon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.js","sourceRoot":"","sources":["../../src/render/icon.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/contracts",
3
- "version": "2026.5.105",
3
+ "version": "2026.5.107",
4
4
  "description": "Domain types, interfaces, and contracts for the CLEO ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -138,7 +138,7 @@
138
138
  "dependencies": {
139
139
  "zod": "^4.3.6",
140
140
  "zod-to-json-schema": "^3.25.2",
141
- "@cleocode/lafs": "2026.5.105"
141
+ "@cleocode/lafs": "2026.5.107"
142
142
  },
143
143
  "scripts": {
144
144
  "build": "tsc -b --force && node scripts/emit-schemas.mjs",
@@ -0,0 +1,138 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { ascii, BadgeIcon, KindIcon, pickIcon, RelationIcon, StatusIcon } from '../icon.js';
3
+
4
+ describe('icon enums', () => {
5
+ it('StatusIcon emoji values match ADR-077', () => {
6
+ expect(StatusIcon.PENDING).toBe('โณ');
7
+ expect(StatusIcon.ACTIVE).toBe('๐Ÿšง');
8
+ expect(StatusIcon.DONE).toBe('โœ…');
9
+ expect(StatusIcon.BLOCKED).toBe('๐Ÿšช');
10
+ expect(StatusIcon.ARCHIVED).toBe('๐Ÿ—„');
11
+ expect(StatusIcon.CANCELLED).toBe('โœ—');
12
+ });
13
+
14
+ it('KindIcon emoji values match ADR-077', () => {
15
+ expect(KindIcon.SAGA).toBe('๐ŸŒฒ');
16
+ expect(KindIcon.EPIC).toBe('๐Ÿ“‹');
17
+ expect(KindIcon.TASK).toBe('โ€ข');
18
+ expect(KindIcon.SUBTASK).toBe('โ—ฆ');
19
+ expect(KindIcon.RESEARCH).toBe('๐Ÿ“–');
20
+ expect(KindIcon.BUG).toBe('๐Ÿ›');
21
+ expect(KindIcon.RELEASE).toBe('๐Ÿš€');
22
+ });
23
+
24
+ it('BadgeIcon emoji values match ADR-077 (ORPHAN amended in T10137)', () => {
25
+ expect(BadgeIcon.EMPTY).toBe('๐Ÿชฆ');
26
+ // ADR-077 ยง2 originally specified '๐Ÿšช' for ORPHAN โ€” identical to
27
+ // StatusIcon.BLOCKED. String enums share runtime values so ascii() cannot
28
+ // disambiguate. Amended to '๐Ÿ‘ป' (abandoned/lonely). T10137 (B12) updates
29
+ // the ADR text to match.
30
+ expect(BadgeIcon.ORPHAN).toBe('๐Ÿ‘ป');
31
+ expect(BadgeIcon.NESTED).toBe('๐Ÿ”');
32
+ expect(BadgeIcon.CAUTION).toBe('โš ');
33
+ expect(BadgeIcon.NEW).toBe('โ˜…');
34
+ });
35
+
36
+ it('RelationIcon emoji values match ADR-077', () => {
37
+ expect(RelationIcon.GROUPS).toBe('โŠ‚');
38
+ expect(RelationIcon.PARENT).toBe('โคด');
39
+ expect(RelationIcon.DEPENDS).toBe('โ‡จ');
40
+ expect(RelationIcon.BLOCKS).toBe('โŠ˜');
41
+ });
42
+
43
+ it('no two enum members share the same runtime string value', () => {
44
+ const all = [
45
+ ...Object.values(StatusIcon),
46
+ ...Object.values(KindIcon),
47
+ ...Object.values(BadgeIcon),
48
+ ...Object.values(RelationIcon),
49
+ ];
50
+ const unique = new Set(all);
51
+ expect(unique.size).toBe(all.length);
52
+ });
53
+ });
54
+
55
+ describe('ascii()', () => {
56
+ it('returns NO_COLOR fallback for every StatusIcon', () => {
57
+ expect(ascii(StatusIcon.PENDING)).toBe('[ ]');
58
+ expect(ascii(StatusIcon.ACTIVE)).toBe('[~]');
59
+ expect(ascii(StatusIcon.DONE)).toBe('[x]');
60
+ expect(ascii(StatusIcon.BLOCKED)).toBe('[!]');
61
+ expect(ascii(StatusIcon.ARCHIVED)).toBe('[#]');
62
+ expect(ascii(StatusIcon.CANCELLED)).toBe('[-]');
63
+ });
64
+
65
+ it('returns NO_COLOR fallback for every KindIcon', () => {
66
+ expect(ascii(KindIcon.SAGA)).toBe('SG');
67
+ expect(ascii(KindIcon.EPIC)).toBe('E');
68
+ expect(ascii(KindIcon.TASK)).toBe('-');
69
+ expect(ascii(KindIcon.SUBTASK)).toBe('.');
70
+ expect(ascii(KindIcon.RESEARCH)).toBe('R');
71
+ expect(ascii(KindIcon.BUG)).toBe('B');
72
+ expect(ascii(KindIcon.RELEASE)).toBe('>');
73
+ });
74
+
75
+ it('returns NO_COLOR fallback for every BadgeIcon', () => {
76
+ expect(ascii(BadgeIcon.EMPTY)).toBe('(empty)');
77
+ expect(ascii(BadgeIcon.ORPHAN)).toBe('(orphan)');
78
+ expect(ascii(BadgeIcon.NESTED)).toBe('(nested)');
79
+ expect(ascii(BadgeIcon.CAUTION)).toBe('(!)');
80
+ expect(ascii(BadgeIcon.NEW)).toBe('(new)');
81
+ });
82
+
83
+ it('returns NO_COLOR fallback for every RelationIcon', () => {
84
+ expect(ascii(RelationIcon.GROUPS)).toBe('in');
85
+ expect(ascii(RelationIcon.PARENT)).toBe('^');
86
+ expect(ascii(RelationIcon.DEPENDS)).toBe('->');
87
+ expect(ascii(RelationIcon.BLOCKS)).toBe('!>');
88
+ });
89
+ });
90
+
91
+ describe('pickIcon()', () => {
92
+ const originalNoColor = process.env['NO_COLOR'];
93
+ const originalTerm = process.env['TERM'];
94
+
95
+ beforeEach(() => {
96
+ delete process.env['NO_COLOR'];
97
+ delete process.env['TERM'];
98
+ });
99
+
100
+ afterEach(() => {
101
+ if (originalNoColor === undefined) {
102
+ delete process.env['NO_COLOR'];
103
+ } else {
104
+ process.env['NO_COLOR'] = originalNoColor;
105
+ }
106
+ if (originalTerm === undefined) {
107
+ delete process.env['TERM'];
108
+ } else {
109
+ process.env['TERM'] = originalTerm;
110
+ }
111
+ });
112
+
113
+ it('returns the emoji when noColor is explicitly false', () => {
114
+ expect(pickIcon(KindIcon.SAGA, { noColor: false })).toBe('๐ŸŒฒ');
115
+ expect(pickIcon(StatusIcon.DONE, { noColor: false })).toBe('โœ…');
116
+ });
117
+
118
+ it('returns the ASCII form when noColor is explicitly true', () => {
119
+ expect(pickIcon(KindIcon.SAGA, { noColor: true })).toBe('SG');
120
+ expect(pickIcon(StatusIcon.DONE, { noColor: true })).toBe('[x]');
121
+ });
122
+
123
+ it('falls back to NO_COLOR=1 from process.env', () => {
124
+ process.env['NO_COLOR'] = '1';
125
+ expect(pickIcon(KindIcon.EPIC)).toBe('E');
126
+ expect(pickIcon(BadgeIcon.ORPHAN)).toBe('(orphan)');
127
+ });
128
+
129
+ it('falls back to TERM=dumb from process.env', () => {
130
+ process.env['TERM'] = 'dumb';
131
+ expect(pickIcon(RelationIcon.GROUPS)).toBe('in');
132
+ });
133
+
134
+ it('returns the emoji when no NO_COLOR signals are present', () => {
135
+ expect(pickIcon(KindIcon.TASK)).toBe('โ€ข');
136
+ expect(pickIcon(StatusIcon.ACTIVE)).toBe('๐Ÿšง');
137
+ });
138
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Typed icon enums for the human-render contract (ADR-077).
3
+ *
4
+ * Single source of truth for the visual language used across all renderers
5
+ * (tree, table, list, badge). Each enum is emoji-first; `ascii()` returns the
6
+ * NO_COLOR-safe fallback for non-UTF-8 terminals or `NO_COLOR=1` environments.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Status icons โ€” agent-task lifecycle states.
13
+ *
14
+ * Emoji-first; `ascii()` returns a NO_COLOR-safe fallback.
15
+ */
16
+ export enum StatusIcon {
17
+ /** Task is queued and has not started. */
18
+ PENDING = 'โณ',
19
+ /** Task is in progress. */
20
+ ACTIVE = '๐Ÿšง',
21
+ /** Task completed successfully. */
22
+ DONE = 'โœ…',
23
+ /** Task is blocked on a dependency or gate. */
24
+ BLOCKED = '๐Ÿšช',
25
+ /** Task is archived and no longer actively tracked. */
26
+ ARCHIVED = '๐Ÿ—„',
27
+ /** Task was cancelled before completion. */
28
+ CANCELLED = 'โœ—',
29
+ }
30
+
31
+ /**
32
+ * Task-kind icons โ€” Saga / Epic / Task / Subtask plus ancillary kinds.
33
+ */
34
+ export enum KindIcon {
35
+ /** Saga โ€” multi-release theme grouping โ‰ฅ 2 Epics. */
36
+ SAGA = '๐ŸŒฒ',
37
+ /** Epic โ€” one releasable slice. */
38
+ EPIC = '๐Ÿ“‹',
39
+ /** Task โ€” one atomic PR-sized change. */
40
+ TASK = 'โ€ข',
41
+ /** Subtask โ€” one commit contributing to a Task's PR. */
42
+ SUBTASK = 'โ—ฆ',
43
+ /** Research task โ€” investigation or audit. */
44
+ RESEARCH = '๐Ÿ“–',
45
+ /** Bug task โ€” defect tracking. */
46
+ BUG = '๐Ÿ›',
47
+ /** Release task โ€” shipping work. */
48
+ RELEASE = '๐Ÿš€',
49
+ }
50
+
51
+ /**
52
+ * Badge icons โ€” flags attached to tasks (empty, orphan, nested, etc).
53
+ */
54
+ export enum BadgeIcon {
55
+ /** Container has no children. */
56
+ EMPTY = '๐Ÿชฆ',
57
+ /**
58
+ * Task has no parent and is unreachable from a Saga or Epic.
59
+ *
60
+ * NOTE: ADR-077 ยง2 originally specified `'๐Ÿšช'`, identical to `StatusIcon.BLOCKED`.
61
+ * TypeScript string enums share a single runtime string so `ascii()` cannot
62
+ * disambiguate. Resolved by switching to `'๐Ÿ‘ป'` (abandoned/lonely semantics).
63
+ * A follow-up ADR amendment lands in T10137 (B12).
64
+ */
65
+ ORPHAN = '๐Ÿ‘ป',
66
+ /** Task is nested deeper than the depth budget allows. */
67
+ NESTED = '๐Ÿ”',
68
+ /** Caution โ€” something needs human attention. */
69
+ CAUTION = 'โš ',
70
+ /** Recently added โ€” surfaced for visibility. */
71
+ NEW = 'โ˜…',
72
+ }
73
+
74
+ /**
75
+ * Relation icons โ€” edge semantics in tree views.
76
+ */
77
+ export enum RelationIcon {
78
+ /** Saga groups Epic via `task_relations.relation_type='groups'`. */
79
+ GROUPS = 'โŠ‚',
80
+ /** Parent edge โ€” direct hierarchical parent. */
81
+ PARENT = 'โคด',
82
+ /** Depends-on edge โ€” A cannot start until B is done. */
83
+ DEPENDS = 'โ‡จ',
84
+ /** Blocks edge โ€” A actively blocks B from progressing. */
85
+ BLOCKS = 'โŠ˜',
86
+ }
87
+
88
+ /**
89
+ * Union of every icon enum โ€” useful as the parameter type for helpers that
90
+ * accept any icon without caring which category it came from.
91
+ */
92
+ export type AnyIcon = StatusIcon | KindIcon | BadgeIcon | RelationIcon;
93
+
94
+ /**
95
+ * Maps an emoji icon to its NO_COLOR-safe ASCII equivalent.
96
+ *
97
+ * Used when stdout is not a UTF-8 TTY or `NO_COLOR=1` is set. Every enum
98
+ * member has an explicit ASCII fallback โ€” the exhaustive switch ensures the
99
+ * TypeScript compiler flags any future icon added without an ASCII mapping.
100
+ *
101
+ * @param icon - Any icon from `StatusIcon`, `KindIcon`, `BadgeIcon`, or `RelationIcon`.
102
+ * @returns The ASCII-only string to render in NO_COLOR contexts.
103
+ */
104
+ export function ascii(icon: AnyIcon): string {
105
+ switch (icon) {
106
+ // Status
107
+ case StatusIcon.PENDING:
108
+ return '[ ]';
109
+ case StatusIcon.ACTIVE:
110
+ return '[~]';
111
+ case StatusIcon.DONE:
112
+ return '[x]';
113
+ case StatusIcon.BLOCKED:
114
+ return '[!]';
115
+ case StatusIcon.ARCHIVED:
116
+ return '[#]';
117
+ case StatusIcon.CANCELLED:
118
+ return '[-]';
119
+ // Kind
120
+ case KindIcon.SAGA:
121
+ return 'SG';
122
+ case KindIcon.EPIC:
123
+ return 'E';
124
+ case KindIcon.TASK:
125
+ return '-';
126
+ case KindIcon.SUBTASK:
127
+ return '.';
128
+ case KindIcon.RESEARCH:
129
+ return 'R';
130
+ case KindIcon.BUG:
131
+ return 'B';
132
+ case KindIcon.RELEASE:
133
+ return '>';
134
+ // Badge
135
+ case BadgeIcon.EMPTY:
136
+ return '(empty)';
137
+ case BadgeIcon.ORPHAN:
138
+ return '(orphan)';
139
+ case BadgeIcon.NESTED:
140
+ return '(nested)';
141
+ case BadgeIcon.CAUTION:
142
+ return '(!)';
143
+ case BadgeIcon.NEW:
144
+ return '(new)';
145
+ // Relation
146
+ case RelationIcon.GROUPS:
147
+ return 'in';
148
+ case RelationIcon.PARENT:
149
+ return '^';
150
+ case RelationIcon.DEPENDS:
151
+ return '->';
152
+ case RelationIcon.BLOCKS:
153
+ return '!>';
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Options for {@link pickIcon}.
159
+ */
160
+ export interface PickIconOptions {
161
+ /**
162
+ * Force ASCII fallback. When omitted, falls back to `NO_COLOR=1` or
163
+ * `TERM=dumb` in `process.env`.
164
+ */
165
+ noColor?: boolean;
166
+ }
167
+
168
+ /**
169
+ * Returns the icon to render given the current environment.
170
+ *
171
+ * Honors `NO_COLOR=1` and `TERM=dumb` for ASCII fallback when `opts.noColor`
172
+ * is not provided explicitly.
173
+ *
174
+ * @param icon - The icon to render.
175
+ * @param opts - Optional override for NO_COLOR detection.
176
+ * @returns The emoji icon, or its ASCII equivalent if NO_COLOR mode is active.
177
+ */
178
+ export function pickIcon<I extends AnyIcon>(icon: I, opts?: PickIconOptions): string {
179
+ const noColor =
180
+ opts?.noColor ?? (process.env['NO_COLOR'] === '1' || process.env['TERM'] === 'dumb');
181
+ return noColor ? ascii(icon) : icon;
182
+ }