@ghl-ai/aw 0.1.84 → 0.1.85

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/c4/slimRouter.mjs CHANGED
@@ -74,15 +74,22 @@ Naming a route is not invoking it. Read = call the Skill tool.
74
74
 
75
75
  | Intent signal in the prompt | Stage skill |
76
76
  |---|---|
77
- | review, "review this PR", findings, governance, "is this ready" | \`aw:review\` |
78
- | plan, design, refactor strategy, propose approach | \`aw:plan\` |
79
- | build, implement, write code, add feature, TDD | \`aw:build\` |
77
+ | review, "review this PR", findings, governance, "is this ready", skeptical validation | \`aw:review\` |
78
+ | plan, design, refactor strategy, propose approach, missing/stale docs | \`aw:plan\` |
79
+ | build, implement, write code, add feature, TDD, approved docs | \`aw:build\` |
80
80
  | bug, crash, alert, regression, debug, investigate | \`aw:investigate\` |
81
- | test, QA, e2e, coverage, regression suite | \`aw:test\` |
81
+ | test, QA, e2e, coverage, regression suite, compatibility proof | \`aw:test\` |
82
82
  | deploy, release, rollout, staging | \`aw:deploy\` |
83
83
  | ship, launch, rollback readiness, closeout | \`aw:ship\` |
84
84
  | feature end-to-end (research → ship) | \`aw:feature\` |
85
- | agent/skill/command/rule authoring | \`aw:adk\` |
85
+ | agent/skill/command/rule authoring, trigger/routing quality, context/token budget | \`aw:adk\` |
86
+
87
+ ## Token budget and evidence packet
88
+
89
+ Default to a bounded packet: prompt, loaded repo rules, existing AW docs/state,
90
+ exact target files/tests, and git diff. Load domain references only after the
91
+ stage skill names them. Stop gathering context once route, repo scope, next
92
+ artifact/command, assumptions, blockers, and validation proof are clear.
86
93
 
87
94
  ## Tier-3 domain skills are delegated, not invoked directly
88
95
 
@@ -141,15 +148,22 @@ Naming a route is not invoking it. Read = open the SKILL.md file.
141
148
 
142
149
  | Intent signal in the prompt | Stage skill |
143
150
  |---|---|
144
- | review, "review this PR", findings, governance, "is this ready" | \`aw-review\` |
145
- | plan, design, refactor strategy, propose approach | \`aw-plan\` |
146
- | build, implement, write code, add feature, TDD | \`aw-build\` |
151
+ | review, "review this PR", findings, governance, "is this ready", skeptical validation | \`aw-review\` |
152
+ | plan, design, refactor strategy, propose approach, missing/stale docs | \`aw-plan\` |
153
+ | build, implement, write code, add feature, TDD, approved docs | \`aw-build\` |
147
154
  | bug, crash, alert, regression, debug, investigate | \`aw-investigate\` |
148
- | test, QA, e2e, coverage, regression suite | \`aw-test\` |
155
+ | test, QA, e2e, coverage, regression suite, compatibility proof | \`aw-test\` |
149
156
  | deploy, release, rollout, staging | \`aw-deploy\` |
150
157
  | ship, launch, rollback readiness, closeout | \`aw-ship\` |
151
158
  | feature end-to-end (research → ship) | \`aw-feature\` |
152
- | agent/skill/command/rule authoring | \`aw-adk\` |
159
+ | agent/skill/command/rule authoring, trigger/routing quality, context/token budget | \`aw-adk\` |
160
+
161
+ ## Token budget and evidence packet
162
+
163
+ Default to a bounded packet: prompt, loaded repo rules, existing AW docs/state,
164
+ exact target files/tests, and git diff. Load domain references only after the
165
+ stage skill names them. Stop gathering context once route, repo scope, next
166
+ artifact/command, assumptions, blockers, and validation proof are clear.
153
167
 
154
168
  ## Tier-3 domain skills are delegated, not invoked directly
155
169
 
package/cli.mjs CHANGED
@@ -33,6 +33,7 @@ const COMMANDS = {
33
33
  'slack-sim': () => import('./commands/slack-sim.mjs').then(m => m.slackSimCommand),
34
34
  c4: () => import('./commands/c4.mjs').then(m => m.c4Command),
35
35
  'init-repo': () => import('./commands/init-repo.mjs').then(m => m.initRepoCommand),
36
+ 'clickup-sync': () => import('./commands/clickup-sync.mjs').then(m => m.clickupSyncCommand),
36
37
  };
37
38
 
38
39
  const COMMAND_NAMES = Object.keys(COMMANDS);
@@ -182,6 +183,8 @@ function printHelp() {
182
183
  cmd('aw integrations add <key>', 'Install a specific tool (e.g. codex, caveman, context-mode)'),
183
184
  cmd('aw integrations remove <key>', 'Remove a tool'),
184
185
  cmd('aw integrations bundle <name>', 'Install a preset bundle'),
186
+ cmd('aw clickup-sync <items.json> --list <id>', 'Sync JSON items into a ClickUp list (create new, comment recurring; input-validated, fail-closed)'),
187
+ cmd('aw clickup-sync <items.json> --list <id> --dry-run', 'Preview the create/comment plan without any writes'),
185
188
  cmd('aw drop <path>', 'Stop syncing or delete local content'),
186
189
  cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
187
190
  cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
@@ -0,0 +1,154 @@
1
+ // clickup-sync.mjs — pure core for `aw clickup-sync`.
2
+ // Schema validation + sentinel-based dedup + sync-plan computation.
3
+ // No I/O: side effects live in commands/clickup-sync.mjs and integrations/clickup.mjs.
4
+ // Contract: .aw_docs/features/aw-clickup-sync/spec.md §3.
5
+
6
+ // Stable item key: leading alnum, then alnum plus . _ - : / (so structured
7
+ // producer keys like `<repo>:<app>:<category>:<file>` are accepted), 1..256 chars.
8
+ // Whitespace and angle brackets stay excluded so the key always round-trips
9
+ // through the dedup sentinel `<!-- aw-sync-key: KEY -->` unambiguously. This is
10
+ // also the drift guard — a key that could not round-trip is refused up front
11
+ // (spec §3.1, §3.2).
12
+ export const KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
13
+
14
+ // Machine-parseable dedup marker embedded in each created task's description.
15
+ export const SENTINEL_RE = /<!--\s*aw-sync-key:\s*(\S+)\s*-->/;
16
+
17
+ // Legacy migration (F2): tasks created by the previous ClickUp path carry the
18
+ // key as a trailing `key:<finding_key>` token in the task NAME (e.g.
19
+ // "... · key:ghl-revex-backend:saas:APPSEC:apps/saas/src/x.ts"), not the
20
+ // description sentinel. Dual-read this so those tasks are recognized (commented,
21
+ // not duplicated) on the first run. The captured token is validated against
22
+ // KEY_RE before use.
23
+ export const LEGACY_NAME_KEY_RE = /key:(\S+)\s*$/;
24
+
25
+ export const TITLE_MAX = 255;
26
+ export const DESC_MAX = 8000;
27
+
28
+ const ALLOWED_FIELDS = new Set(['key', 'title', 'description']);
29
+
30
+ /**
31
+ * Validate untrusted, already-parsed JSON into a normalized item list.
32
+ * Fail-closed: throws a clear Error on any shape/field/format violation.
33
+ * An empty array is valid and returns []. No mutation of the input.
34
+ * @param {unknown} raw
35
+ * @returns {Array<{ key: string, title: string, description: string }>}
36
+ */
37
+ export function parseAndValidateItems(raw) {
38
+ if (!Array.isArray(raw)) {
39
+ throw new Error('Invalid items file: top-level JSON must be an array of items.');
40
+ }
41
+
42
+ const seenKeys = new Set();
43
+ const items = raw.map((item, index) => {
44
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) {
45
+ throw new Error(`Invalid item ${index}: expected an object with key, title, description.`);
46
+ }
47
+
48
+ for (const field of Object.keys(item)) {
49
+ if (!ALLOWED_FIELDS.has(field)) {
50
+ throw new Error(`Invalid item ${index}: unknown field "${field}" (allowed: key, title, description).`);
51
+ }
52
+ }
53
+
54
+ const { key, title } = item;
55
+ if (typeof key !== 'string') {
56
+ throw new Error(`Invalid item ${index}: "key" is required and must be a string.`);
57
+ }
58
+ if (!KEY_RE.test(key)) {
59
+ throw new Error(`Invalid item ${index}: "key" "${key}" must match ${KEY_RE} (alphanumeric start; . _ - allowed; max 64).`);
60
+ }
61
+ if (seenKeys.has(key)) {
62
+ throw new Error(`Invalid items file: duplicate key "${key}" (keys must be unique to avoid duplicate tasks).`);
63
+ }
64
+ seenKeys.add(key);
65
+
66
+ if (typeof title !== 'string' || title.trim().length === 0) {
67
+ throw new Error(`Invalid item ${index}: "title" is required and must be a non-empty string.`);
68
+ }
69
+ if (title.length > TITLE_MAX) {
70
+ throw new Error(`Invalid item ${index}: "title" exceeds ${TITLE_MAX} characters.`);
71
+ }
72
+
73
+ const description = item.description === undefined ? '' : item.description;
74
+ if (typeof description !== 'string') {
75
+ throw new Error(`Invalid item ${index}: "description" must be a string when present.`);
76
+ }
77
+ if (description.length > DESC_MAX) {
78
+ throw new Error(`Invalid item ${index}: "description" exceeds ${DESC_MAX} characters.`);
79
+ }
80
+
81
+ return { key, title, description };
82
+ });
83
+
84
+ return items;
85
+ }
86
+
87
+ /**
88
+ * Append the dedup sentinel to a task description.
89
+ * @param {string} description
90
+ * @param {string} key
91
+ * @returns {string}
92
+ */
93
+ export function embedSentinel(description, key) {
94
+ const base = description ? `${description}\n\n` : '';
95
+ return `${base}<!-- aw-sync-key: ${key} -->`;
96
+ }
97
+
98
+ /**
99
+ * Extract the embedded key from an existing ClickUp task, or null if absent.
100
+ * Reads `description` first, falling back to `text_content`.
101
+ * @param {{ description?: string, text_content?: string }} task
102
+ * @returns {string | null}
103
+ */
104
+ export function extractKey(task) {
105
+ if (!task || typeof task !== 'object') return null;
106
+
107
+ // 1. New format: description sentinel (authoritative).
108
+ const source = typeof task.description === 'string' && task.description.length > 0
109
+ ? task.description
110
+ : (typeof task.text_content === 'string' ? task.text_content : '');
111
+ const match = source.match(SENTINEL_RE);
112
+ if (match) return match[1];
113
+
114
+ // 2. Legacy fallback: trailing `key:<finding_key>` token in the task name.
115
+ if (typeof task.name === 'string') {
116
+ const legacy = task.name.match(LEGACY_NAME_KEY_RE);
117
+ if (legacy && KEY_RE.test(legacy[1])) return legacy[1];
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Split items into new (create) vs recurring (comment) against existing tasks.
125
+ * Pure — inputs are not mutated.
126
+ * @param {Array<{ key: string, title: string, description: string }>} items
127
+ * @param {Array<Record<string, unknown>>} existingTasks
128
+ * @returns {{ toCreate: typeof items, toComment: Array<{ item: (typeof items)[number], taskId: string }> }}
129
+ */
130
+ export function computeSyncPlan(items, existingTasks) {
131
+ const keyToTaskId = new Map();
132
+ for (const task of existingTasks || []) {
133
+ const key = extractKey(task);
134
+ // Guard against a sentinel-bearing task with no id: skip it rather than map
135
+ // to the literal "undefined" (would later comment on a bogus task id). The
136
+ // item then falls through to create, which is the safe outcome.
137
+ if (key !== null && task.id != null && !keyToTaskId.has(key)) {
138
+ keyToTaskId.set(key, String(task.id));
139
+ }
140
+ }
141
+
142
+ const toCreate = [];
143
+ const toComment = [];
144
+ for (const item of items) {
145
+ const taskId = keyToTaskId.get(item.key);
146
+ if (taskId === undefined) {
147
+ toCreate.push(item);
148
+ } else {
149
+ toComment.push({ item, taskId });
150
+ }
151
+ }
152
+
153
+ return { toCreate, toComment };
154
+ }
@@ -0,0 +1,120 @@
1
+ // commands/clickup-sync.mjs — `aw clickup-sync <items.json> --list <listId> [--dry-run]`
2
+ //
3
+ // Syncs a JSON items file into a ClickUp list: create new tasks, comment on
4
+ // recurring ones, deduped by an embedded description sentinel. Fail-closed on
5
+ // bad input (validated before any ClickUp write); empty list is a clean no-op;
6
+ // --dry-run performs zero writes. Contract: spec.md §5, §6.
7
+ //
8
+ // fs + client are injected via `deps` for testability; the CLI passes none, so
9
+ // the real readFileSync + createClickUpClient defaults apply.
10
+
11
+ import { readFileSync } from 'node:fs';
12
+ import * as fmt from '../fmt.mjs';
13
+ import { chalk } from '../fmt.mjs';
14
+ import { createClickUpClient } from '../integrations/clickup.mjs';
15
+ import { parseAndValidateItems, embedSentinel, computeSyncPlan } from '../clickup-sync.mjs';
16
+
17
+ const RECURRING_COMMENT = 'Recurred via `aw clickup-sync` — item seen again in the source list.';
18
+
19
+ export async function clickupSyncCommand(args, deps = {}) {
20
+ const readFile = deps.readFile ?? ((path) => readFileSync(path, 'utf8'));
21
+ const makeClient = deps.createClient ?? (() => createClickUpClient());
22
+
23
+ fmt.configureInteractionMode(args);
24
+ fmt.intro('aw clickup-sync');
25
+
26
+ // ── Args ──
27
+ const filePath = (args._positional || [])[0];
28
+ if (!filePath) {
29
+ fmt.cancel('Missing items file. Usage: aw clickup-sync <items.json> --list <listId> [--dry-run]');
30
+ }
31
+ const listId = args['--list'];
32
+ if (!listId || listId === true) {
33
+ fmt.cancel('Missing --list <listId>. Usage: aw clickup-sync <items.json> --list <listId> [--dry-run]');
34
+ }
35
+ const dryRun = args['--dry-run'] === true;
36
+
37
+ // ── Read → parse → validate (all before any network / write) ──
38
+ let rawText;
39
+ try {
40
+ rawText = readFile(filePath);
41
+ } catch (err) {
42
+ fmt.cancel(`Cannot read items file "${filePath}": ${errMessage(err)}`);
43
+ }
44
+
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(rawText);
48
+ } catch (err) {
49
+ fmt.cancel(`Invalid JSON in "${filePath}": ${errMessage(err)}`);
50
+ }
51
+
52
+ let items;
53
+ try {
54
+ items = parseAndValidateItems(parsed);
55
+ } catch (err) {
56
+ fmt.cancel(errMessage(err));
57
+ }
58
+
59
+ if (items.length === 0) {
60
+ fmt.outro('Nothing to sync (empty items list).');
61
+ return { created: 0, commented: 0, dryRun };
62
+ }
63
+
64
+ // ── Fetch existing tasks + compute plan (fail-closed on read errors) ──
65
+ const client = makeClient();
66
+ let existingTasks;
67
+ try {
68
+ existingTasks = await client.listAllTasks(listId);
69
+ } catch (err) {
70
+ fmt.cancel(`Failed to read existing tasks from list ${listId}: ${errMessage(err)}`);
71
+ }
72
+
73
+ const { toCreate, toComment } = computeSyncPlan(items, existingTasks);
74
+
75
+ if (dryRun) {
76
+ fmt.note(
77
+ [
78
+ `${chalk.green(`${toCreate.length} to create`)} ${chalk.cyan(`${toComment.length} to comment`)}`,
79
+ '',
80
+ ...toCreate.map((i) => `${chalk.green('+ create')} ${i.key} — ${i.title}`),
81
+ ...toComment.map((c) => `${chalk.cyan('~ comment')} ${c.item.key} → task ${c.taskId}`),
82
+ ].join('\n'),
83
+ 'Dry run — no writes',
84
+ );
85
+ fmt.outro(`Dry run complete: would create ${toCreate.length}, comment ${toComment.length}.`);
86
+ return { created: 0, commented: 0, dryRun: true };
87
+ }
88
+
89
+ // ── Execute: fail-fast. Input was validated atomically; the write phase is
90
+ // fail-fast + idempotent-resumable (a re-run skips created tasks). ──
91
+ let created = 0;
92
+ for (const item of toCreate) {
93
+ try {
94
+ await client.createTask(listId, {
95
+ name: item.title,
96
+ description: embedSentinel(item.description, item.key),
97
+ });
98
+ created++;
99
+ } catch (err) {
100
+ fmt.cancel(`Failed creating task for "${item.key}" after ${created} created (re-run to resume): ${errMessage(err)}`);
101
+ }
102
+ }
103
+
104
+ let commented = 0;
105
+ for (const { item, taskId } of toComment) {
106
+ try {
107
+ await client.addComment(taskId, RECURRING_COMMENT);
108
+ commented++;
109
+ } catch (err) {
110
+ fmt.cancel(`Failed commenting on task ${taskId} for "${item.key}": ${errMessage(err)}`);
111
+ }
112
+ }
113
+
114
+ fmt.outro(`${chalk.green(`${created} created`)} · ${chalk.cyan(`${commented} commented`)}`);
115
+ return { created, commented, dryRun: false };
116
+ }
117
+
118
+ function errMessage(err) {
119
+ return err instanceof Error ? err.message : String(err);
120
+ }
@@ -0,0 +1,137 @@
1
+ // integrations/clickup.mjs — ClickUp v2 HTTP client for the aw CLI.
2
+ //
3
+ // Faithful .mjs port of apps/api/src/mcp/integrations/clickup.handler.ts, which
4
+ // remains the source of truth for base URL, headers, 10s timeout, and the
5
+ // throw-on-non-ok contract. The CLI runs standalone ESM (no TS build), so the
6
+ // handler cannot be imported directly. Additions over the handler: page
7
+ // accumulation with a hard cap and bounded 429 backoff (spec §3.3, §3.4).
8
+ //
9
+ // A factory (createClickUpClient) injects token + sleep so behavior is unit
10
+ // testable with a mocked global fetch and no real timers.
11
+
12
+ export const CLICKUP_BASE = 'https://api.clickup.com/api/v2';
13
+ export const MAX_PAGES = 200;
14
+ export const MAX_RETRIES = 3;
15
+ // The handler uses 10s for interactive single-task MCP calls. Bulk list paging
16
+ // over a large list is much slower (observed ~40s for one 100-task page), so
17
+ // the CLI uses a far larger per-request budget, overridable via env for very
18
+ // large lists. (spec §3.3)
19
+ export const DEFAULT_TIMEOUT_MS = 120_000;
20
+ const ENV_TIMEOUT_MS = Number(process.env.AW_CLICKUP_TIMEOUT_MS);
21
+ const REQUEST_TIMEOUT_MS = Number.isFinite(ENV_TIMEOUT_MS) && ENV_TIMEOUT_MS > 0
22
+ ? ENV_TIMEOUT_MS
23
+ : DEFAULT_TIMEOUT_MS;
24
+ const MAX_BACKOFF_MS = 60_000;
25
+
26
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27
+
28
+ // Resolve a 429 backoff delay (ms) from Retry-After (seconds) or
29
+ // X-RateLimit-Reset (epoch seconds), clamped to [0, 60]s. Defaults to 1s.
30
+ function resolveRetryDelayMs(res) {
31
+ const retryAfter = res.headers.get('retry-after');
32
+ if (retryAfter !== null && retryAfter !== undefined && retryAfter !== '') {
33
+ const seconds = Number(retryAfter);
34
+ if (Number.isFinite(seconds)) return clampMs(seconds * 1000);
35
+ }
36
+ const reset = res.headers.get('x-ratelimit-reset');
37
+ if (reset !== null && reset !== undefined && reset !== '') {
38
+ const resetEpoch = Number(reset);
39
+ if (Number.isFinite(resetEpoch)) return clampMs(resetEpoch * 1000 - Date.now());
40
+ }
41
+ return 1000;
42
+ }
43
+
44
+ function clampMs(ms) {
45
+ if (!Number.isFinite(ms) || ms < 0) return 0;
46
+ return Math.min(ms, MAX_BACKOFF_MS);
47
+ }
48
+
49
+ /**
50
+ * @param {{ token?: string, sleep?: (ms: number) => Promise<void> }} [opts]
51
+ */
52
+ export function createClickUpClient(opts = {}) {
53
+ const token = opts.token ?? process.env.CLICKUP_API_TOKEN ?? '';
54
+ const sleep = opts.sleep ?? realSleep;
55
+
56
+ async function clickupFetch(path, init) {
57
+ if (!token) {
58
+ throw new Error('CLICKUP_API_TOKEN not set. Export a ClickUp API token before running clickup-sync.');
59
+ }
60
+
61
+ const method = init?.method ?? 'GET';
62
+ // A transport failure (timeout / reset) is ambiguous — the request may have
63
+ // been processed server-side. Retrying is only safe for idempotent GETs;
64
+ // retrying a POST (createTask / addComment) could duplicate a task or comment
65
+ // that actually succeeded. Non-GET transport failures fail fast; the sync is
66
+ // cross-run resumable, so the next run re-pulls and skips via the sentinel.
67
+ const retriableTransport = method === 'GET';
68
+
69
+ for (let attempt = 0; ; attempt++) {
70
+ let res;
71
+ try {
72
+ res = await fetch(`${CLICKUP_BASE}${path}`, {
73
+ method,
74
+ headers: { Authorization: token, 'Content-Type': 'application/json' },
75
+ body: init?.body,
76
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
77
+ });
78
+ } catch (err) {
79
+ // Large ClickUp lists have high, variable per-page latency, so retry
80
+ // transient GET failures with a bounded backoff before giving up.
81
+ if (retriableTransport && attempt < MAX_RETRIES) {
82
+ await sleep(Math.min((attempt + 1) * 2000, MAX_BACKOFF_MS));
83
+ continue;
84
+ }
85
+ const suffix = retriableTransport ? ` after ${MAX_RETRIES + 1} attempts` : ' (not retried; non-idempotent request)';
86
+ throw new Error(`ClickUp ${method} request failed${suffix}: ${err instanceof Error ? err.message : String(err)}`);
87
+ }
88
+
89
+ if (res.ok) return res.json();
90
+
91
+ // 429 means the request was rejected before side effects, so it is safe to
92
+ // retry for any method; every other non-2xx fails fast.
93
+ if (res.status === 429 && attempt < MAX_RETRIES) {
94
+ await sleep(resolveRetryDelayMs(res));
95
+ continue;
96
+ }
97
+
98
+ const text = await res.text();
99
+ throw new Error(`ClickUp API ${res.status}: ${text.slice(0, 500)}`);
100
+ }
101
+ }
102
+
103
+ // Page through every task in a list (spec §3.3). include_closed so completed
104
+ // prior tasks are not recreated; subtasks so nested tasks are matched too.
105
+ async function listAllTasks(listId) {
106
+ const tasks = [];
107
+ for (let page = 0; page < MAX_PAGES; page++) {
108
+ const params = new URLSearchParams({
109
+ page: String(page),
110
+ include_closed: 'true',
111
+ subtasks: 'true',
112
+ });
113
+ const body = await clickupFetch(`/list/${encodeURIComponent(listId)}/task?${params}`);
114
+ const pageTasks = Array.isArray(body?.tasks) ? body.tasks : [];
115
+ if (pageTasks.length === 0) return tasks;
116
+ tasks.push(...pageTasks);
117
+ if (body?.last_page === true) return tasks;
118
+ }
119
+ throw new Error(`ClickUp list ${listId} exceeded the ${MAX_PAGES}-page cap; aborting to avoid duplicate creates.`);
120
+ }
121
+
122
+ async function createTask(listId, { name, description }) {
123
+ return clickupFetch(`/list/${encodeURIComponent(listId)}/task`, {
124
+ method: 'POST',
125
+ body: JSON.stringify({ name, description }),
126
+ });
127
+ }
128
+
129
+ async function addComment(taskId, commentText) {
130
+ return clickupFetch(`/task/${encodeURIComponent(taskId)}/comment`, {
131
+ method: 'POST',
132
+ body: JSON.stringify({ comment_text: commentText }),
133
+ });
134
+ }
135
+
136
+ return { clickupFetch, listAllTasks, createTask, addComment };
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin.js",
11
11
  "c4/",
12
12
  "cli.mjs",
13
+ "clickup-sync.mjs",
13
14
  "commands/",
14
15
  "config.mjs",
15
16
  "codex.mjs",
package/render-rules.mjs CHANGED
@@ -398,7 +398,7 @@ Is the user authoring/editing a CASRE artifact (command, agent, skill, rule, eva
398
398
  └── NO → /aw-build
399
399
  \`\`\`
400
400
 
401
- **ADK triggers**: "create an agent/skill/command/rule/eval", "score my skill", "audit all agents", "ADK", "developer kit", "fix lint errors" on registry artifacts.
401
+ **ADK triggers**: "create/improve an agent/skill/command/rule/eval", "score my skill", "audit all agents", "ADK", "developer kit", "fix lint errors" on registry artifacts, trigger/routing quality, skill context size, token budget, evidence packets, progressive disclosure, or stop conditions for AW skills.
402
402
 
403
403
  **Publish triggers**: "push this agent/skill/rule to the registry", "publish registry changes", "sync to platform-docs", "aw push", "aw push-rules". NOT regular git push, app PRs, or deploys.
404
404
 
@@ -412,6 +412,12 @@ Is the user authoring/editing a CASRE artifact (command, agent, skill, rule, eva
412
412
  - /aw-build → code changes with tests.
413
413
  - /aw-investigate → reproduction + root cause evidence.
414
414
 
415
+ ## Token Budget Model
416
+
417
+ - Start from a bounded evidence packet: prompt, repo instructions, existing AW docs/state, exact target files/tests, and git diff.
418
+ - Load bulky platform/domain references only after the selected stage skill or touched files require them.
419
+ - Stop context gathering once route, repo scope, next artifact/command, assumptions, blockers, and validation proof are clear.
420
+
415
421
  ## Org Rules
416
422
 
417
423
  After the stage skill is loaded, also read the relevant org rules: