td-ai-tools 1.2.2 → 1.2.3

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
@@ -57,6 +57,8 @@ This keeps the installed assets available to both Claude-style and `.agents`-sty
57
57
 
58
58
  Some skills provide a recognized setup command (`setup.sh`, `scripts/setup.sh`, or `package.json` with `scripts.setup`). Non-interactive installs and updates run setup only when you pass `--setup`; interactive installs/updates ask for confirmation only when the selected skills include a recognized setup command. Accepted setup runs once inside each installed copy: `.claude/skills/<name>/` and `.agents/skills/<name>/`.
59
59
 
60
+ Skills can also declare reusable project-hook setup in `hooks/manifest.json`. The shared installer in `lib/project-hooks.js` copies listed hook assets and merges each registration into its Claude or Codex JSON settings file, preserving unrelated configuration and updating an existing hook with the same `type` and `command` instead of duplicating it. A hook manifest counts as a recognized setup even when the skill has no setup script, so future skills only need to provide their assets and declarative registrations.
61
+
60
62
  `install` now errors when the target item already exists. Use `update` to replace an existing installed skill or agent pack.
61
63
 
62
64
  `delete` removes installed items from both `.claude/` and `.agents/` target directories, and works on any installed skill or agent pack regardless of whether it is in the catalogue.
package/lib/installer.js CHANGED
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { copyDir } from './fs-utils.js';
11
11
  import { bundledAgentsIn } from './catalog.js';
12
+ import { hasProjectHooks, installProjectHooks } from './project-hooks.js';
12
13
 
13
14
  /** @typedef {import('./catalog.js').Ctx} Ctx */
14
15
  /** @typedef {import('./catalog.js').Target} Target */
@@ -66,7 +67,8 @@ export function findSkillSetup(skillDir) {
66
67
  * @returns {boolean}
67
68
  */
68
69
  export function skillHasSetup(ctx, name) {
69
- return Boolean(findSkillSetup(path.join(ctx.skillsDir, name)));
70
+ const skillDir = path.join(ctx.skillsDir, name);
71
+ return Boolean(findSkillSetup(skillDir)) || hasProjectHooks(skillDir);
70
72
  }
71
73
 
72
74
  /**
@@ -75,28 +77,36 @@ export function skillHasSetup(ctx, name) {
75
77
  * @param {string} name - Skill name.
76
78
  * @param {Target} target - The destination layout.
77
79
  * @param {string} dest - Installed skill directory.
80
+ * @param {string} projectRoot - Root of the project receiving the skill.
78
81
  * @param {ReportFn} report
79
82
  * @returns {boolean} Whether setup succeeded or no setup existed.
80
83
  */
81
- function runSkillSetup(name, target, dest, report) {
84
+ function runSkillSetup(name, target, dest, projectRoot, report) {
82
85
  const setup = findSkillSetup(dest);
83
- if (!setup) return true;
84
-
85
- report('step', `setup: ${name} running ${setup.command} ${setup.args.join(' ')} in ${target.root}/skills/${name}/`);
86
- const result = spawnSync(setup.command, setup.args, {
87
- cwd: dest,
88
- stdio: 'inherit',
89
- shell: false,
90
- });
91
- if (result.error) {
92
- report('error', `setup: ${name} failed in ${target.root}/skills/${name}/: ${result.error.message}`);
93
- return false;
86
+ if (setup) {
87
+ report('step', `setup: ${name} running ${setup.command} ${setup.args.join(' ')} in ${target.root}/skills/${name}/`);
88
+ const result = spawnSync(setup.command, setup.args, {
89
+ cwd: dest,
90
+ stdio: 'inherit',
91
+ shell: false,
92
+ });
93
+ if (result.error) {
94
+ report('error', `setup: ${name} failed in ${target.root}/skills/${name}/: ${result.error.message}`);
95
+ return false;
96
+ }
97
+ if (result.status !== 0) {
98
+ report('error', `setup: ${name} failed in ${target.root}/skills/${name}/ with exit code ${result.status}`);
99
+ return false;
100
+ }
101
+ report('success', `setup: ${name} completed in ${target.root}/skills/${name}/`);
94
102
  }
95
- if (result.status !== 0) {
96
- report('error', `setup: ${name} failed in ${target.root}/skills/${name}/ with exit code ${result.status}`);
103
+
104
+ try {
105
+ installProjectHooks(projectRoot, dest, { report });
106
+ } catch (error) {
107
+ report('error', `setup: ${name} hook installation failed: ${error.message}`);
97
108
  return false;
98
109
  }
99
- report('success', `setup: ${name} completed in ${target.root}/skills/${name}/`);
100
110
  return true;
101
111
  }
102
112
 
@@ -150,7 +160,7 @@ export function installSkill(ctx, name, { replaceExisting = false, runSetup = fa
150
160
  copyDir(src, dest);
151
161
  report('success', `skill: ${name} ${replaceExisting ? 'updated' : 'installed'} → ${target.root}/skills/${name}/`);
152
162
  registerBundledAgents(ctx, target, src, report);
153
- if (runSetup && !runSkillSetup(name, target, dest, report)) ok = false;
163
+ if (runSetup && !runSkillSetup(name, target, dest, ctx.targetRoot, report)) ok = false;
154
164
  }
155
165
  return ok;
156
166
  }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @file Declarative, non-destructive project hook installation for skills.
3
+ *
4
+ * Skills opt in by shipping `hooks/manifest.json`. Hook assets are copied from
5
+ * that directory and registrations are merged into runtime JSON settings.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ const MANIFEST_PATH = path.join('hooks', 'manifest.json');
11
+
12
+ /** @type {(kind: 'success'|'warn'|'info', message: string) => void} */
13
+ const noop = () => {};
14
+
15
+ /**
16
+ * Whether a skill directory contains a project hook manifest.
17
+ *
18
+ * @param {string} skillDir
19
+ * @returns {boolean}
20
+ */
21
+ export function hasProjectHooks(skillDir) {
22
+ return fs.existsSync(path.join(skillDir, MANIFEST_PATH));
23
+ }
24
+
25
+ /**
26
+ * Resolve a manifest path while preventing traversal outside its allowed root.
27
+ *
28
+ * @param {string} root
29
+ * @param {string} relativePath
30
+ * @param {string} label
31
+ * @returns {string}
32
+ */
33
+ function resolveWithin(root, relativePath, label) {
34
+ if (typeof relativePath !== 'string' || !relativePath || path.isAbsolute(relativePath)) {
35
+ throw new Error(`${label} must be a non-empty relative path`);
36
+ }
37
+ const resolvedRoot = path.resolve(root);
38
+ const resolved = path.resolve(resolvedRoot, relativePath);
39
+ const relation = path.relative(resolvedRoot, resolved);
40
+ if (relation === '..' || relation.startsWith(`..${path.sep}`) || path.isAbsolute(relation)) {
41
+ throw new Error(`${label} resolves outside the project root: ${relativePath}`);
42
+ }
43
+ return resolved;
44
+ }
45
+
46
+ /**
47
+ * Merge one runtime hook into a settings object.
48
+ *
49
+ * Command hooks are identified by `type` + `command`. Reinstalling updates the
50
+ * managed hook in place while preserving unrelated settings, event groups,
51
+ * group fields, hook fields, and sibling hooks.
52
+ *
53
+ * @param {Record<string, any>} settings
54
+ * @param {string} event
55
+ * @param {Record<string, any>} hook
56
+ * @param {Record<string, any>} [group]
57
+ * @returns {void}
58
+ */
59
+ export function mergeHookRegistration(settings, event, hook, group = {}) {
60
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
61
+ throw new Error('hook settings must be a JSON object');
62
+ }
63
+ if (typeof event !== 'string' || !event) {
64
+ throw new Error('hook event must be a non-empty string');
65
+ }
66
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)
67
+ || typeof hook.type !== 'string' || typeof hook.command !== 'string') {
68
+ throw new Error('hook must define string type and command fields');
69
+ }
70
+ if (!group || typeof group !== 'object' || Array.isArray(group)) {
71
+ throw new Error('hook group must be a JSON object');
72
+ }
73
+
74
+ if (settings.hooks === undefined) settings.hooks = {};
75
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
76
+ throw new Error('settings.hooks must be a JSON object');
77
+ }
78
+ if (settings.hooks[event] === undefined) settings.hooks[event] = [];
79
+ if (!Array.isArray(settings.hooks[event])) {
80
+ throw new Error(`settings.hooks.${event} must be an array`);
81
+ }
82
+
83
+ for (const existingGroup of settings.hooks[event]) {
84
+ if (!existingGroup || !Array.isArray(existingGroup.hooks)) continue;
85
+ const existingHook = existingGroup.hooks.find(candidate => (
86
+ candidate?.type === hook.type && candidate?.command === hook.command
87
+ ));
88
+ if (!existingHook) continue;
89
+ Object.assign(existingGroup, group);
90
+ Object.assign(existingHook, hook);
91
+ return;
92
+ }
93
+
94
+ settings.hooks[event].push({ ...group, hooks: [{ ...hook }] });
95
+ }
96
+
97
+ /**
98
+ * Read JSON settings, backing up malformed input before starting a clean file.
99
+ *
100
+ * @param {string} settingsPath
101
+ * @param {(kind: 'success'|'warn'|'info', message: string) => void} report
102
+ * @returns {Record<string, any>}
103
+ */
104
+ function readSettings(settingsPath, report) {
105
+ if (!fs.existsSync(settingsPath)) return {};
106
+ try {
107
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
108
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
109
+ throw new Error('root value is not an object');
110
+ }
111
+ return settings;
112
+ } catch (error) {
113
+ const backupPath = `${settingsPath}.bak`;
114
+ fs.copyFileSync(settingsPath, backupPath);
115
+ report('warn', `hooks: ${settingsPath} was invalid JSON; backed up to ${backupPath}`);
116
+ return {};
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Install the hook assets and registrations declared by a skill.
122
+ *
123
+ * Manifest shape:
124
+ * `{ files: [{ source, destination }], registrations: [{ settings, event,
125
+ * hook, group? }] }`. File sources are relative to `hooks/`; destinations and
126
+ * settings paths are relative to the installing project.
127
+ *
128
+ * @param {string} projectRoot
129
+ * @param {string} skillDir
130
+ * @param {object} [options]
131
+ * @param {(kind: 'success'|'warn'|'info', message: string) => void} [options.report]
132
+ * @returns {boolean} Whether a manifest was found and installed.
133
+ */
134
+ export function installProjectHooks(projectRoot, skillDir, { report = noop } = {}) {
135
+ const manifestPath = path.join(skillDir, MANIFEST_PATH);
136
+ if (!fs.existsSync(manifestPath)) return false;
137
+
138
+ const hooksDir = path.dirname(manifestPath);
139
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
140
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
141
+ throw new Error(`hook manifest must be a JSON object: ${manifestPath}`);
142
+ }
143
+
144
+ for (const file of manifest.files || []) {
145
+ const source = resolveWithin(hooksDir, file.source, 'hook source');
146
+ const destination = resolveWithin(projectRoot, file.destination, 'hook destination');
147
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
148
+ throw new Error(`hook source does not exist: ${source}`);
149
+ }
150
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
151
+ fs.copyFileSync(source, destination);
152
+ fs.chmodSync(destination, fs.statSync(source).mode);
153
+ report('success', `hooks: installed ${file.destination}`);
154
+ }
155
+
156
+ for (const registration of manifest.registrations || []) {
157
+ const settingsPath = resolveWithin(projectRoot, registration.settings, 'hook settings path');
158
+ const settings = readSettings(settingsPath, report);
159
+ mergeHookRegistration(
160
+ settings,
161
+ registration.event,
162
+ registration.hook,
163
+ registration.group || {},
164
+ );
165
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
166
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
167
+ report('success', `hooks: merged ${registration.event} into ${registration.settings}`);
168
+ }
169
+
170
+ return true;
171
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test",
7
+ "test": "node --test test/*.test.js",
8
8
  "readmes": "node scripts/sync-readmes.js",
9
9
  "readmes:check": "node scripts/sync-readmes.js --check",
10
10
  "smoke:install": "./scripts/smoke-install.sh"
package/skills/README.md CHANGED
@@ -6,10 +6,8 @@
6
6
  - `basecamp`: Interact with Basecamp via the Basecamp CLI.
7
7
  - `browser-validation`: Before completing a task validate frontend or template changes in a real browser with the Playwright-CLI…
8
8
  - `cache-reset`: Clear and warm Laravel and Statamic caches (including Statamic Glide image caches) after content or template…
9
- - `car-ticket-generator`: Generate a ticket for the codex-auto-runner queue
10
9
  - `client-overview`: Generate a client-facing markdown report that summarizes all changes on the current branch against the…
11
10
  - `debugging-ios-webkit`: Debugs iOS Safari/Chrome-iOS rendering bugs — stale paints, viewport/browser-chrome clipping, mobile-only CSS…
12
- - `everhour-basecamp-estimates`: Bulk update Everhour task estimates from a Basecamp todo or todolist URL, then append bracketed hours to the…
13
11
  - `forge-cli`: Manage Laravel Forge servers, sites, and provisioned resources from the terminal with the Laravel Forge CLI,…
14
12
  - `horizon-component-migration`: Bundle Shopify Horizon components into a migration package for a different theme, including recursive…
15
13
  - `playwright-cli`: Automates browser interactions for web testing, screenshots, and data extraction.
@@ -18,7 +16,7 @@
18
16
  - `pull-request-statamic`: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and…
19
17
  - `record-changes`: Update `docs/changes.md` by summarizing the current branch against the primary development branch.
20
18
  - `shopify-cli`: Shopify CLI workflows for theme development.
21
- - `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on offenses in files modified on the current Git branch.
19
+ - `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on
22
20
  - `stylesheet-migration`: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts.
23
21
  - `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify theme work.
24
22
  - `td-review`: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings…
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: shopify-lint
3
- version: 1.0.0
3
+ version: 1.1.0
4
4
  description: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on offenses in files modified on the current Git branch. Use when Codex needs to lint a Shopify theme, validate branch-scoped Liquid or theme changes, enforce Theory theme rules, or avoid surfacing pre-existing Theme Check offenses from untouched files.
5
5
  ---
6
6
 
@@ -18,6 +18,9 @@ Installing with `td-ai-tools install --setup shopify-lint` runs this automatical
18
18
 
19
19
  1. Runs `npm install && npm run build` inside `theme-check-theory/`, producing `dist/index.js` — the CommonJS entry point the root `.theme-check.yml` requires.
20
20
  2. Writes a `.theme-check.yml` at the project root wiring in the bundled checks. If one already exists it is left untouched; ensure its `require:` list includes `./.agents/skills/shopify-lint/theme-check-theory`.
21
+ 3. Through the installer's shared project-hook setup, installs matching Claude and Codex Stop hooks and merges their registrations into `.claude/settings.json` and `.codex/hooks.json` without replacing unrelated settings or hooks. Re-running setup updates the Shopify Lint hook in place instead of duplicating it.
22
+
23
+ The Stop hooks run this skill's branch-scoped lint before handoff, return modified-file offenses to the agent, and allow handoff after three unsuccessful fix rounds. They require `jq`, `python3`, and Shopify CLI to be available when the hook runs. Directly running `scripts/setup.sh` performs the package build and Theme Check configuration; use `td-ai-tools install --setup` or `td-ai-tools update --setup` to apply the shared hook manifest as well.
21
24
 
22
25
  Keep these dependencies inside `theme-check-theory/node_modules`; do not install Node dependencies at the theme root.
23
26
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: run Theory's branch-scoped Shopify Theme Check before the agent
3
+ # hands off. Blocks the stop (feeding offenses back to the agent) when lint
4
+ # finds offenses in files modified on the current branch.
5
+ set -u
6
+
7
+ input=$(cat)
8
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // "unknown"')
9
+
10
+ root="${CLAUDE_PROJECT_DIR:-$PWD}"
11
+ cd "$root" || exit 0
12
+
13
+ # Only lint when this checkout is a Shopify theme with the skill installed.
14
+ lint_script=".agents/skills/shopify-lint/scripts/shopify_lint.py"
15
+ [ -f "$lint_script" ] || exit 0
16
+
17
+ # Cap consecutive blocks so an offense the agent can't fix doesn't loop forever.
18
+ counter_file="${TMPDIR:-/tmp}/claude-shopify-lint-stop-${session_id}.count"
19
+
20
+ output=$(python3 "$lint_script" --path . 2>&1)
21
+ status=$?
22
+
23
+ if [ "$status" -eq 0 ]; then
24
+ rm -f "$counter_file"
25
+ exit 0
26
+ fi
27
+
28
+ if [ "$status" -eq 1 ]; then
29
+ count=0
30
+ [ -f "$counter_file" ] && count=$(cat "$counter_file" 2>/dev/null || echo 0)
31
+ count=$((count + 1))
32
+ printf '%s' "$count" > "$counter_file"
33
+
34
+ if [ "$count" -gt 3 ]; then
35
+ rm -f "$counter_file"
36
+ jq -n --arg msg "Shopify lint still failing after 3 fix rounds — allowing handoff. Remaining offenses:\n$output" \
37
+ '{systemMessage: $msg}'
38
+ exit 0
39
+ fi
40
+
41
+ jq -n --arg reason "Shopify Theme Check found offenses in files modified on this branch. Fix these before finishing (leave offenses in unmodified files alone — they are pre-existing), then stop again and the check will re-run:
42
+
43
+ $output" '{decision: "block", reason: $reason}'
44
+ exit 0
45
+ fi
46
+
47
+ # Exit 2 = Git/CLI/JSON tooling error. Don't block handoff on a broken toolchain.
48
+ jq -n --arg msg "Shopify lint hook could not run (exit $status): $output" '{systemMessage: $msg}'
49
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: run Theory's branch-scoped Shopify Theme Check before the agent
3
+ # hands off. Continues the turn with offenses from files modified on the
4
+ # current branch so Codex can fix them before trying to stop again.
5
+ set -u
6
+
7
+ input=$(cat)
8
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // "unknown"' | tr -cd '[:alnum:]_-')
9
+
10
+ root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
11
+ cd "$root" || exit 0
12
+
13
+ # Only lint when this checkout is a Shopify theme with the skill installed.
14
+ lint_script=".agents/skills/shopify-lint/scripts/shopify_lint.py"
15
+ [ -f "$lint_script" ] || exit 0
16
+
17
+ # Cap consecutive continuations so an offense the agent can't fix doesn't loop forever.
18
+ counter_file="${TMPDIR:-/tmp}/codex-shopify-lint-stop-${session_id:-unknown}.count"
19
+
20
+ output=$(python3 "$lint_script" --path . 2>&1)
21
+ status=$?
22
+
23
+ if [ "$status" -eq 0 ]; then
24
+ rm -f "$counter_file"
25
+ exit 0
26
+ fi
27
+
28
+ if [ "$status" -eq 1 ]; then
29
+ count=0
30
+ [ -f "$counter_file" ] && count=$(cat "$counter_file" 2>/dev/null || echo 0)
31
+ count=$((count + 1))
32
+ printf '%s' "$count" > "$counter_file"
33
+
34
+ if [ "$count" -gt 3 ]; then
35
+ rm -f "$counter_file"
36
+ jq -n --arg msg "Shopify lint still failing after 3 fix rounds — allowing handoff. Remaining offenses:\n$output" \
37
+ '{systemMessage: $msg}'
38
+ exit 0
39
+ fi
40
+
41
+ jq -n --arg reason "Shopify Theme Check found offenses in files modified on this branch. Fix these before finishing (leave offenses in unmodified files alone — they are pre-existing), then stop again and the check will re-run:
42
+
43
+ $output" '{continue: false, stopReason: $reason}'
44
+ exit 0
45
+ fi
46
+
47
+ # Exit 2 = Git/CLI/JSON tooling error. Don't continue the turn on a broken toolchain.
48
+ jq -n --arg msg "Shopify lint hook could not run (exit $status): $output" '{systemMessage: $msg}'
49
+ exit 0
@@ -0,0 +1,34 @@
1
+ {
2
+ "files": [
3
+ {
4
+ "source": "claude-stop.sh",
5
+ "destination": ".claude/hooks/shopify-lint-stop.sh"
6
+ },
7
+ {
8
+ "source": "codex-stop.sh",
9
+ "destination": ".codex/hooks/td-shopify-lint-stop.sh"
10
+ }
11
+ ],
12
+ "registrations": [
13
+ {
14
+ "settings": ".claude/settings.json",
15
+ "event": "Stop",
16
+ "hook": {
17
+ "type": "command",
18
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/shopify-lint-stop.sh\"",
19
+ "timeout": 300,
20
+ "statusMessage": "Running Shopify Theme Check on branch changes..."
21
+ }
22
+ },
23
+ {
24
+ "settings": ".codex/hooks.json",
25
+ "event": "Stop",
26
+ "hook": {
27
+ "type": "command",
28
+ "command": "bash \"$(git rev-parse --show-toplevel)/.codex/hooks/td-shopify-lint-stop.sh\"",
29
+ "timeout": 300,
30
+ "statusMessage": "Running Shopify Theme Check on branch changes..."
31
+ }
32
+ }
33
+ ]
34
+ }
@@ -9,6 +9,8 @@ set -euo pipefail
9
9
  # (its node_modules and dist/ are intentionally not committed).
10
10
  # 2. Writes a `.theme-check.yml` at the project root that wires the bundled
11
11
  # checks into Shopify CLI Theme Check.
12
+ # 3. When run by td-ai-tools, its shared setup stage installs the hook assets
13
+ # declared in hooks/manifest.json and merges their Stop registrations.
12
14
 
13
15
  SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
14
16
  PKG_DIR="$SKILL_DIR/theme-check-theory"
@@ -1,131 +0,0 @@
1
- ---
2
- name: car-ticket-generator
3
- version: 1.0.0
4
- description: Generate a ticket for the codex-auto-runner queue
5
- alwaysApply: false
6
- ---
7
-
8
- # CAR Ticket Skill
9
-
10
- Generate a ticket for the Codex-Auto-Runner ticket queue
11
-
12
- ---
13
-
14
- ## 1) Ticket files
15
-
16
- - Filename: `TICKET-###*.md`
17
- Examples: `TICKET-001.md`, `TICKET-120-api-parity.md`
18
- - Tickets **must be in ascending numeric order**.
19
- - Numbers **do not need to be consecutive**.
20
- Leave gaps if follow-up tickets are likely.
21
- - Location: `.codex-autorunner/tickets`
22
-
23
- ---
24
-
25
- ## 2) Required frontmatter (minimum)
26
-
27
- ```yaml
28
- ---
29
- agent: 'codex'
30
- done: false
31
- ---
32
- ```
33
-
34
- Required (linted):
35
-
36
- - `agent`: registered CAR agent id for this repo (for example `codex`, `opencode`) or the special value `user`
37
- - `done`: boolean
38
-
39
- Do not use assistant product names as `agent` values (for example `chatgpt` or `claude`) unless those exact ids are configured in CAR for this repo.
40
-
41
- Common optional fields:
42
-
43
- - `title`: short, outcome-focused summary
44
- - `goal`: one-sentence statement of intent
45
- - `model`: pin a specific model when necessary
46
-
47
- ---
48
-
49
- ## 3) Recommended ticket body structure
50
-
51
- Keep tickets concise and independently verifiable.
52
-
53
- Preferred sections:
54
-
55
- - `## Tasks` - concrete implementation steps
56
- - `## Acceptance criteria` (or `## Exit criteria`) - observable outcomes
57
- - `## Tests` - commands or explicit verification steps
58
- - `## Notes` - only if necessary
59
-
60
- Write criteria so another agent can prove completion without guesswork.
61
-
62
- ---
63
-
64
- ## 4) Sequencing rules (critical)
65
-
66
- CAR's `ticket_flow`:
67
-
68
- - Executes tickets in ascending order
69
- - Picks the first ticket where `done != true`
70
-
71
- Implications:
72
-
73
- - Put prerequisites in lower-numbered tickets.
74
- - Never make a lower-numbered ticket depend on a higher-numbered one.
75
- - If reverse dependencies appear, reorder or split tickets.
76
- - Each ticket must be independently completable when its turn arrives.
77
-
78
- ---
79
-
80
- ## 5) Assignment defaults
81
-
82
- - Implementation work -> repo agents (`codex`, `opencode`, etc.).
83
- - Final human review/signoff -> user-assigned ticket near the end.
84
- - In PMA mode, prefer delegation, not direct code edits.
85
-
86
- ---
87
-
88
- ## 6) Quality bar for good tickets
89
-
90
- A good ticket:
91
-
92
- - Has a single, well-scoped outcome
93
- - References specific files/modules when useful
94
- - Includes explicit verification (tests, checks, or observable behavior)
95
- - Avoids vague language (`"improve"`, `"clean up"`, `"fix issues"`) without criteria
96
-
97
- ---
98
-
99
- ## 7) Copy-paste ticket template
100
-
101
- ```md
102
- ---
103
- title: '<Outcome-focused title>'
104
- agent: 'codex'
105
- done: false
106
- goal: '<What will be true when this ticket is complete>.'
107
- ---
108
-
109
- ## Tasks
110
-
111
- - <Concrete implementation step>
112
- - <Concrete implementation step>
113
-
114
- ## Acceptance criteria
115
-
116
- - <Observable behavior or artifact>
117
- - <Observable behavior or artifact>
118
-
119
- ## Tests
120
-
121
- - <Commands to run or explicit checks>
122
- ```
123
-
124
- ---
125
-
126
- ## 8) Anti-patterns to reject
127
-
128
- - Missing or invalid frontmatter
129
- - Cross-ticket dependency deadlocks
130
- - Tickets that rely on unstated or hidden context
131
- - `done: true` without evidence against acceptance criteria
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: 'Car Ticket Generator'
3
- short_description: 'Generate a car ticket for the user'
4
- default_prompt: 'Generate a car ticket for the user'
@@ -1,2 +0,0 @@
1
- EVERHOUR_API_KEY=replace-with-everhour-api-key
2
- EVERHOUR_PROJECT_ID=ev:1234567890