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 +2 -0
- package/lib/installer.js +27 -17
- package/lib/project-hooks.js +171 -0
- package/package.json +2 -2
- package/skills/README.md +1 -3
- package/skills/shopify-lint/SKILL.md +4 -1
- package/skills/shopify-lint/hooks/claude-stop.sh +49 -0
- package/skills/shopify-lint/hooks/codex-stop.sh +49 -0
- package/skills/shopify-lint/hooks/manifest.json +34 -0
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/scripts/setup.sh +2 -0
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/car-ticket-generator/SKILL.md +0 -131
- package/skills/car-ticket-generator/agents/openai.yaml +0 -4
- package/skills/everhour-basecamp-estimates/.env.example +0 -2
- package/skills/everhour-basecamp-estimates/SKILL.md +0 -75
- package/skills/everhour-basecamp-estimates/agents/openai.yaml +0 -4
- package/skills/everhour-basecamp-estimates/scripts/update_estimates.py +0 -654
- package/skills/everhour-basecamp-estimates/tests/test_update_estimates.py +0 -286
- package/skills/playwright-cli/PRIMER.md +0 -122
- package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
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
|
-
|
|
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 (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|