@ethlete/agent-rules 0.1.0-next.3 → 0.1.0-next.31
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/AGENTS.md +28 -0
- package/CHANGELOG.md +480 -0
- package/CLAUDE.md +1 -0
- package/README.md +362 -29
- package/THIRD-PARTY-LICENSES.md +35 -0
- package/content/defaults.json +1 -1
- package/content/git-hooks/post-checkout.sh +10 -0
- package/content/git-hooks/pre-push.sh +5 -0
- package/content/hooks/context-warning.py +872 -110
- package/content/hooks/subagent-model-policy.py +194 -0
- package/content/mods/.claude-plugin/marketplace.json +11 -0
- package/content/mods/plugins/e2e-progress/.claude-plugin/plugin.json +6 -0
- package/content/mods/plugins/e2e-progress/hooks/hooks.json +1 -0
- package/content/mods/plugins/e2e-progress/hooks/register.tsx +442 -0
- package/content/mods/plugins/e2e-progress/tsconfig.json +3 -0
- package/content/mods/plugins/e2e-progress/types/index.d.ts +23 -0
- package/content/output-styles/ste-clarity.md +131 -0
- package/content/rules/agent-sessions-wake-up.md +11 -0
- package/content/rules/app-styling.md +66 -0
- package/content/rules/comments.md +4 -1
- package/content/rules/lint-and-format.md +7 -5
- package/content/rules/nx-layout.md +50 -0
- package/content/rules/reactive-state.md +1 -1
- package/content/rules/styling.md +9 -7
- package/content/rules/subagent-models.md +39 -0
- package/content/skills/agent-sessions/SKILL.md +81 -0
- package/content/skills/agent-sessions/agent-sessions.mjs +498 -0
- package/content/skills/angular-patterns/SKILL.md +30 -7
- package/content/skills/api-source/SKILL.md +156 -0
- package/content/skills/app-testing/SKILL.md +141 -0
- package/content/skills/codex-subagent/SKILL.md +90 -0
- package/content/skills/codex-subagent/codex-agent.mjs +229 -0
- package/content/skills/design-exploration/SKILL.md +687 -0
- package/content/skills/design-exploration/check-story.mjs +139 -0
- package/content/skills/design-exploration/shoot-template.mjs +61 -0
- package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/content/skills/domain-modeling/SKILL.md +80 -0
- package/content/skills/e2e-triage/SKILL.md +80 -0
- package/content/skills/figma-export/SKILL.md +191 -0
- package/content/skills/figma-export/dump-figma-layers.py +83 -0
- package/content/skills/figma-export/dump-figma-svg.py +235 -0
- package/content/skills/figma-export/measure-template.mjs +87 -0
- package/content/skills/git-commit/SKILL.md +11 -7
- package/content/skills/git-flow/SKILL.md +87 -0
- package/content/skills/grill-with-docs/SKILL.md +13 -0
- package/content/skills/grilling/SKILL.md +51 -0
- package/content/skills/handoff/SKILL.md +125 -5
- package/content/skills/query/SKILL.md +165 -64
- package/content/skills/rxjs-signals/SKILL.md +33 -4
- package/content/skills/sdk-docs/SKILL.md +50 -14
- package/content/skills/sdk-local-build/SKILL.md +26 -8
- package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
- package/content/skills/sdk-source/SKILL.md +37 -34
- package/content/skills/sdk-update/SKILL.md +103 -0
- package/content/skills/story-styling/SKILL.md +10 -8
- package/content/skills/styleguide/SKILL.md +12 -7
- package/content/skills/styleguide/assets.md +54 -0
- package/content/skills/styleguide/changesets.md +17 -0
- package/content/skills/styleguide/file-structure.md +73 -0
- package/content/skills/styleguide/lint-rule-lookup.md +51 -0
- package/content/skills/styleguide/storybook-structure.md +25 -0
- package/content/skills/theming/SKILL.md +50 -28
- package/content/skills/timetrack/SKILL.md +397 -0
- package/content/skills/verify-in-app/SKILL.md +107 -0
- package/migrations/app-styling-utilities.md +63 -0
- package/migrations/list-state-query-form.md +20 -0
- package/migrations/nx-layout.md +29 -0
- package/migrations/sdk-components-over-hand-built-ui.md +27 -0
- package/migrations/search-query-field.md +52 -0
- package/migrations.json +48 -0
- package/package.json +19 -3
- package/src/index.js +85 -30
- package/src/index.js.map +1 -1
- package/src/lib/cli-args.d.ts +9 -0
- package/src/lib/cli-args.js +48 -0
- package/src/lib/cli-args.js.map +1 -0
- package/src/lib/commitlint.d.ts +10 -0
- package/src/lib/commitlint.js +51 -0
- package/src/lib/commitlint.js.map +1 -0
- package/src/lib/config.d.ts +41 -5
- package/src/lib/config.js +117 -10
- package/src/lib/config.js.map +1 -1
- package/src/lib/filter.d.ts +2 -0
- package/src/lib/filter.js +4 -4
- package/src/lib/filter.js.map +1 -1
- package/src/lib/frontmatter.d.ts +2 -0
- package/src/lib/frontmatter.js +33 -5
- package/src/lib/frontmatter.js.map +1 -1
- package/src/lib/git-flow/build.d.ts +35 -0
- package/src/lib/git-flow/build.js +24 -0
- package/src/lib/git-flow/build.js.map +1 -0
- package/src/lib/git-flow/config.d.ts +59 -0
- package/src/lib/git-flow/config.js +50 -0
- package/src/lib/git-flow/config.js.map +1 -0
- package/src/lib/git-flow/index.d.ts +6 -0
- package/src/lib/git-flow/index.js +23 -0
- package/src/lib/git-flow/index.js.map +1 -0
- package/src/lib/git-flow/parse.d.ts +49 -0
- package/src/lib/git-flow/parse.js +300 -0
- package/src/lib/git-flow/parse.js.map +1 -0
- package/src/lib/git-flow/rename.d.ts +24 -0
- package/src/lib/git-flow/rename.js +70 -0
- package/src/lib/git-flow/rename.js.map +1 -0
- package/src/lib/git-flow/start.d.ts +49 -0
- package/src/lib/git-flow/start.js +57 -0
- package/src/lib/git-flow/start.js.map +1 -0
- package/src/lib/git-flow/validate.d.ts +34 -0
- package/src/lib/git-flow/validate.js +72 -0
- package/src/lib/git-flow/validate.js.map +1 -0
- package/src/lib/git-flow-command.d.ts +4 -0
- package/src/lib/git-flow-command.js +165 -0
- package/src/lib/git-flow-command.js.map +1 -0
- package/src/lib/git-flow-repair.d.ts +17 -0
- package/src/lib/git-flow-repair.js +165 -0
- package/src/lib/git-flow-repair.js.map +1 -0
- package/src/lib/git-flow-start.d.ts +20 -0
- package/src/lib/git-flow-start.js +140 -0
- package/src/lib/git-flow-start.js.map +1 -0
- package/src/lib/git.d.ts +64 -0
- package/src/lib/git.js +131 -0
- package/src/lib/git.js.map +1 -0
- package/src/lib/gitlab.d.ts +35 -0
- package/src/lib/gitlab.js +116 -0
- package/src/lib/gitlab.js.map +1 -0
- package/src/lib/index.d.ts +2 -0
- package/src/lib/index.js +26 -11
- package/src/lib/index.js.map +1 -1
- package/src/lib/migrate.js +6 -1
- package/src/lib/migrate.js.map +1 -1
- package/src/lib/output-style-command.d.ts +3 -0
- package/src/lib/output-style-command.js +69 -0
- package/src/lib/output-style-command.js.map +1 -0
- package/src/lib/output-style.d.ts +38 -0
- package/src/lib/output-style.js +134 -0
- package/src/lib/output-style.js.map +1 -0
- package/src/lib/owned-paths.js +27 -3
- package/src/lib/owned-paths.js.map +1 -1
- package/src/lib/package-runner.d.ts +4 -0
- package/src/lib/package-runner.js +86 -0
- package/src/lib/package-runner.js.map +1 -0
- package/src/lib/plain-text.d.ts +9 -0
- package/src/lib/plain-text.js +22 -0
- package/src/lib/plain-text.js.map +1 -0
- package/src/lib/plan.d.ts +6 -6
- package/src/lib/plan.js +164 -40
- package/src/lib/plan.js.map +1 -1
- package/src/lib/prompt.d.ts +8 -0
- package/src/lib/prompt.js +35 -0
- package/src/lib/prompt.js.map +1 -0
- package/src/lib/render.d.ts +25 -2
- package/src/lib/render.js +43 -10
- package/src/lib/render.js.map +1 -1
- package/src/lib/sessions-command.d.ts +4 -0
- package/src/lib/sessions-command.js +16 -0
- package/src/lib/sessions-command.js.map +1 -0
- package/src/lib/sync.d.ts +0 -1
- package/src/lib/sync.js +30 -17
- package/src/lib/sync.js.map +1 -1
- package/src/lib/targets/agents-skills.js +1 -1
- package/src/lib/targets/agents-skills.js.map +1 -1
- package/src/lib/targets/claude-hooks.d.ts +3 -23
- package/src/lib/targets/claude-hooks.js +22 -86
- package/src/lib/targets/claude-hooks.js.map +1 -1
- package/src/lib/targets/claude-mods.d.ts +20 -0
- package/src/lib/targets/claude-mods.js +47 -0
- package/src/lib/targets/claude-mods.js.map +1 -0
- package/src/lib/targets/claude.js +2 -3
- package/src/lib/targets/claude.js.map +1 -1
- package/src/lib/targets/codex-hooks.d.ts +12 -0
- package/src/lib/targets/codex-hooks.js +35 -0
- package/src/lib/targets/codex-hooks.js.map +1 -0
- package/src/lib/targets/codex.js +4 -4
- package/src/lib/targets/codex.js.map +1 -1
- package/src/lib/targets/copilot.js +4 -4
- package/src/lib/targets/copilot.js.map +1 -1
- package/src/lib/targets/cursor.js +3 -3
- package/src/lib/targets/cursor.js.map +1 -1
- package/src/lib/targets/git-hooks.d.ts +25 -0
- package/src/lib/targets/git-hooks.js +71 -0
- package/src/lib/targets/git-hooks.js.map +1 -0
- package/src/lib/targets/hooks-shared.d.ts +60 -0
- package/src/lib/targets/hooks-shared.js +130 -0
- package/src/lib/targets/hooks-shared.js.map +1 -0
- package/src/lib/targets/mcp.d.ts +16 -0
- package/src/lib/targets/mcp.js +68 -0
- package/src/lib/targets/mcp.js.map +1 -0
- package/src/lib/targets/shared.d.ts +1 -9
- package/src/lib/targets/shared.js +4 -8
- package/src/lib/targets/shared.js.map +1 -1
- package/src/lib/timetrack-command.d.ts +11 -0
- package/src/lib/timetrack-command.js +1063 -0
- package/src/lib/timetrack-command.js.map +1 -0
- package/src/lib/timetrack-snapshot.d.ts +55 -0
- package/src/lib/timetrack-snapshot.js +120 -0
- package/src/lib/timetrack-snapshot.js.map +1 -0
- package/src/lib/timetrack.d.ts +561 -0
- package/src/lib/timetrack.js +279 -0
- package/src/lib/timetrack.js.map +1 -0
- package/content/skills/styleguide/STYLEGUIDE.md +0 -520
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Storybook structure
|
|
2
|
+
|
|
3
|
+
Read this file when placing a `*.stories.ts` file or code used only to render stories.
|
|
4
|
+
|
|
5
|
+
- Keep the `*.stories.ts` file beside the component it documents.
|
|
6
|
+
- Put private host components, dummy data, and story-only helpers in `storybook/`.
|
|
7
|
+
- Never export Storybook-only code from the component's public barrel.
|
|
8
|
+
- A private `storybook/index.ts` may simplify imports into the adjacent story, but no
|
|
9
|
+
production source may import it.
|
|
10
|
+
|
|
11
|
+
```plaintext
|
|
12
|
+
settings-form/
|
|
13
|
+
├── storybook/
|
|
14
|
+
│ ├── settings-form-storybook.component.ts
|
|
15
|
+
│ ├── settings-form-storybook.component.html
|
|
16
|
+
│ ├── settings-form-dummy-data.ts
|
|
17
|
+
│ └── index.ts
|
|
18
|
+
├── settings-form.component.ts
|
|
19
|
+
├── settings-form.component.html
|
|
20
|
+
├── settings-form.component.stories.ts
|
|
21
|
+
└── index.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The story file stays at the component root; only its private support code goes into
|
|
25
|
+
`storybook/`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: theming
|
|
3
|
-
description: The two runtime theming systems in @ethlete/core - surface theming (elevation-aware neutrals) and color theming (semantic accent palettes)
|
|
3
|
+
description: The two runtime theming systems in @ethlete/core - surface theming (elevation-aware neutrals) and color theming (semantic accent palettes). Read BEFORE writing or reviewing CSS involving color, background, border, or interaction state, and when wiring theme context across overlay/portal boundaries.
|
|
4
4
|
kind: skill
|
|
5
5
|
scope: consumer
|
|
6
6
|
requires: ['@ethlete/core']
|
|
@@ -37,16 +37,16 @@ The generated CSS defines the raw vars on `.et-surface--<name>` / `.et-color--<n
|
|
|
37
37
|
classes (plus `:root` defaults) and derives the public tokens below on `:root` **and**
|
|
38
38
|
every scope class - so the tokens are always resolvable and components just read them.
|
|
39
39
|
|
|
40
|
-
**Theme names are yours.**
|
|
41
|
-
|
|
42
|
-
The portable handle is the theme **`type`**: `
|
|
43
|
-
the app registered with `type: 'error'`.
|
|
40
|
+
**Theme names are yours.** Never hardcode them as an SDK-defined union or reusable API
|
|
41
|
+
contract. App-specific docs may name their own themes when they label them as app values.
|
|
42
|
+
The portable handle is the theme **`type`**: `injectSemanticColorTheme('error')` finds
|
|
43
|
+
whichever theme the app registered with `type: 'error'`.
|
|
44
44
|
|
|
45
45
|
## Tokens components may consume
|
|
46
46
|
|
|
47
47
|
Use these **derived** tokens - never the raw `--et-surface-background` /
|
|
48
|
-
`--et-color-primary` channel triplets
|
|
49
|
-
|
|
48
|
+
`--et-color-primary` channel triplets. Never use a hardcoded color as the primary value.
|
|
49
|
+
A static fallback inside `var(--token, <fallback>)` is permitted, but not required.
|
|
50
50
|
|
|
51
51
|
Surface (each exists as `-solid` = usable color, `-rgb` = `R G B` channels):
|
|
52
52
|
|
|
@@ -61,22 +61,26 @@ Surface (each exists as `-solid` = usable color, `-rgb` = `R G B` channels):
|
|
|
61
61
|
|
|
62
62
|
Color (from the nearest `[etProvideColor]` scope):
|
|
63
63
|
|
|
64
|
-
| Token | Use for
|
|
65
|
-
| -------------------------------- |
|
|
66
|
-
| `--et-theme-color-primary` | filled backgrounds
|
|
67
|
-
| `--et-theme-color-primary-solid` | accents at full strength: focus borders, selected marks, spinners
|
|
68
|
-
| `--et-theme-color-on-primary` | text/icons on a primary-filled background
|
|
69
|
-
| `--et-theme-color-ink-solid` | primary-tinted text/border on transparent/tonal fills
|
|
64
|
+
| Token | Use for |
|
|
65
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `--et-theme-color-primary` | filled backgrounds on an element you do **not** tint. To tint per state, set `--et-theme-color-primary-opacity: 0.16` and compose the color yourself: `rgb(var(--et-theme-color-primary-rgb) / var(--et-theme-color-primary-opacity))`. The alias is substituted at the `et-color--*` scope, so an inherited value carries the scope's opacity, never the element's |
|
|
67
|
+
| `--et-theme-color-primary-solid` | accents at full strength: focus borders, selected marks, spinners |
|
|
68
|
+
| `--et-theme-color-on-primary` | text/icons on a primary-filled background |
|
|
69
|
+
| `--et-theme-color-ink-solid` | primary-tinted text/border on transparent/tonal fills |
|
|
70
70
|
|
|
71
71
|
Interaction-state variants (`--et-surface-interaction-{hover,focus,active,disabled}-solid`)
|
|
72
72
|
resolve automatically per CSS state when the element has `[etSurfaceInteractive]`;
|
|
73
73
|
similarly `[etColorInteractive]` re-resolves the color tokens per state. Put either on
|
|
74
74
|
the interactive element itself, never on a wrapper.
|
|
75
75
|
|
|
76
|
+
**Never use `hover:bg-et-surface-interaction-hover` for a hover row.** The token is a
|
|
77
|
+
full-strength neutral tint, so in dark mode it gives a light row with near-white text. Use
|
|
78
|
+
`[etSurfaceInteractive]` on the row for hover feedback.
|
|
79
|
+
|
|
76
80
|
## Providing context
|
|
77
81
|
|
|
78
|
-
- `[etProvideSurface]="
|
|
79
|
-
elevation) set the surface scope; `[etProvideColor]="
|
|
82
|
+
- `[etProvideSurface]="surfaceThemeName"` / `[etAutoSurface]` (auto-picks the next
|
|
83
|
+
elevation) set the surface scope; `[etProvideColor]="colorThemeName"` sets the color scope.
|
|
80
84
|
A component that owns a themed region usually adds `ProvideColorDirective` /
|
|
81
85
|
`ProvideSurfaceDirective` as `hostDirectives`.
|
|
82
86
|
- **Detached overlay panes (menu, dialog, tooltip…) do not inherit DOM context.**
|
|
@@ -85,10 +89,22 @@ the interactive element itself, never on a wrapper.
|
|
|
85
89
|
color and `resolveSurfaceByElevation(themes, type, elevation + 1)` for the surface.
|
|
86
90
|
- **Semantic colors come through DI, not CSS.** There is no global "error color"
|
|
87
91
|
variable - error/warning/success are just color themes. To render something in the
|
|
88
|
-
error color, `
|
|
89
|
-
theme) and either bind it
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
error color, `injectSemanticColorTheme('error')` (reading it throws if the app
|
|
93
|
+
registered no `type: 'error'` theme) and either bind it
|
|
94
|
+
(`[etProvideColor]="errorColorTheme()"`) or force it programmatically
|
|
95
|
+
(`provideColor.forceColor(theme)` / `clearForcedColor()`). Inside that scope,
|
|
96
|
+
`--et-theme-color-primary-*` _is_ the error color.
|
|
97
|
+
- **A surface swaps a chosen accent; it never picks one.** The chosen accent is the
|
|
98
|
+
nearest `[etProvideColor]`, else the `isDefault` theme, or for
|
|
99
|
+
`injectSemanticColorTheme(type)` the theme of that `type`. The nearest surface's
|
|
100
|
+
`SurfaceTheme.colorThemeVariants` (e.g. `{ brand: 'brand-on-light', success: 'success-on-light' }`,
|
|
101
|
+
variants registered without a `type`) swaps it; an accent without an entry stays the
|
|
102
|
+
same on every surface. With no surface above, the root surface swaps, and
|
|
103
|
+
`provideColorScheme()` paints its swap of the default accent on `<html>`. A neutral that follows the surface is `color="surface"`
|
|
104
|
+
(`interactionColor`), not a variant. `colorTheme` and `semanticColorThemes` are
|
|
105
|
+
deprecated. Never add per-surface logic to a color theme or new CSS vars for it.
|
|
106
|
+
`injectErrorTheme()`, `injectWarningTheme()` and `injectSuccessTheme()` are
|
|
107
|
+
deprecated: they ignore the surface.
|
|
92
108
|
|
|
93
109
|
## Pitfalls
|
|
94
110
|
|
|
@@ -96,15 +112,21 @@ the interactive element itself, never on a wrapper.
|
|
|
96
112
|
declared via `@property` can never default to a theme token. If the default should
|
|
97
113
|
come from the theme, don't declare an `@property` - consume the theme token directly
|
|
98
114
|
(optionally behind a `--_et-*` indirection var).
|
|
115
|
+
- `@property` `initial-value` also cannot use a **font-relative or container unit**
|
|
116
|
+
(`em`, `rem`, `ex`, `ch`, `lh`, `cap`, `ic`, `cq*`). The value must be computationally
|
|
117
|
+
independent, so the browser drops the whole rule and leaves the token unregistered -
|
|
118
|
+
with no error. Percentages and viewport units (`vh`, `vw`, `dvh`) are fine. When the
|
|
119
|
+
default has to be relative, use `syntax: '*'` with no `initial-value` and put the
|
|
120
|
+
default in a fallback at each use site: `var(--et-skeleton-size, 1em)`.
|
|
99
121
|
- `--et-theme-color-primary-*` always resolves to the **nearest color scope**. A
|
|
100
122
|
hardcoded semantic color in CSS can't be replaced by it unless the right theme is
|
|
101
123
|
provided on that element.
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
a hard requirement wherever it is
|
|
105
|
-
- **Cascade layers, not `:where()`,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Styling a **story** file rather than a component? The colour rule is the same
|
|
110
|
-
|
|
124
|
+
- A static fallback (`var(--et-surface-border-solid, rgb(255 255 255 / 0.1))`) is
|
|
125
|
+
permitted for theme-less setups, but themes make it unnecessary. `injectSemanticColorTheme()`
|
|
126
|
+
is a hard requirement wherever it is read.
|
|
127
|
+
- **Cascade layers, not `:where()`, decide who wins against SDK component CSS.** SDK
|
|
128
|
+
styles live in `@layer components`, so a utility or unlayered app CSS overrides them.
|
|
129
|
+
An app rule inside `@layer components` loses. See the `app-styling` rule.
|
|
130
|
+
|
|
131
|
+
Styling a **story** file rather than a component? The colour rule is the same. Check the
|
|
132
|
+
repository's Storybook guidance before assuming a Tailwind utility exists.
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: timetrack
|
|
3
|
+
description: How to reach Jira from any repo through the running Timetrack app - look up an issue, search for one, ask which project a repo logs into, file a ticket, add a worklog row, read the user's own Tempo worklogs, read their Google Calendar, read the evidence a day holds, or list the work that still waits for a ticket. Read whenever a task needs Jira data, a Jira write, or the day's own events, and never put a Jira token in a repo.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reaching Jira through Timetrack
|
|
9
|
+
|
|
10
|
+
**No repository holds a Jira token.** The Timetrack desktop app holds one set of credentials
|
|
11
|
+
in this machine's keychain, and every repository asks it:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
{%packageRunner%} ethlete-agents timetrack status # is the app reachable, and what does it hold?
|
|
15
|
+
{%packageRunner%} ethlete-agents timetrack issue FIP-2177 # one issue: summary, type, parent, subject
|
|
16
|
+
{%packageRunner%} ethlete-agents timetrack search "password" # open issues of the picked projects
|
|
17
|
+
{%packageRunner%} ethlete-agents timetrack project # which project does this repo log into?
|
|
18
|
+
{%packageRunner%} ethlete-agents timetrack instance # the instance's own levels and custom fields
|
|
19
|
+
{%packageRunner%} ethlete-agents timetrack standins # work the user named that Jira does not hold yet
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add `--json` to any of them when you need to read a field rather than a line.
|
|
23
|
+
|
|
24
|
+
**Every write waits for the user's approval in the app.** `create`, `log`, `edit`, `standins
|
|
25
|
+
--remove | --rename | --merge | --resolve | --split --force`, `sync --write`, `worklog --delete` and `resync` answer at
|
|
26
|
+
once with an approval id and write nothing yet. The user approves or rejects the request in
|
|
27
|
+
Timetrack; `approval <id>` reads the outcome:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
{%packageRunner%} ethlete-agents timetrack approval <id> # queued, approved (with the result), rejected or expired
|
|
31
|
+
{%packageRunner%} ethlete-agents timetrack approvals # every write that waits now: id, asker, op, asked at, what it writes
|
|
32
|
+
{%packageRunner%} ethlete-agents timetrack approval <id> --reject # reject one that waits, as the app's Reject button does
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A reject answers at once and writes nothing to Jira or Tempo; an item that no longer waits is
|
|
36
|
+
refused and stays as it is. Reject only what the user asked you to, or a write you queued by
|
|
37
|
+
mistake. **There is no approve command and never will be: only the user approves, in the app.**
|
|
38
|
+
|
|
39
|
+
`ask` does what the app's "Ask auto mode again" does, on any day, without moving the day on
|
|
40
|
+
screen. It answers at once with what it queued; the model runs on the user's machine, and its new
|
|
41
|
+
answer replaces the stored one. It writes nothing to Jira or Tempo itself: a ticket or a name the
|
|
42
|
+
answer proposes waits for the user's approval as auto mode's own does.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
{%packageRunner%} ethlete-agents timetrack ask --stand-in <id> [--day YYYY-MM-DD] # an open stand-in, ids from `standins`
|
|
46
|
+
{%packageRunner%} ethlete-agents timetrack ask --context <context-id> [--day YYYY-MM-DD] # an unnamed band, ids from `rows`
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A subject the app does not offer the press on is refused: an unknown or resolved stand-in, one
|
|
50
|
+
the day holds no time of, a context a rule names or the user named by hand.
|
|
51
|
+
|
|
52
|
+
Tell the user a request waits in Timetrack, then read `approval` once they say they decided -
|
|
53
|
+
do not poll in a loop. A request that is still waiting at the end of the day it was asked on
|
|
54
|
+
expires; ask again rather than assuming it landed. Set `TIMETRACK_CLIENT` to name yourself in the
|
|
55
|
+
queue (Claude Code is named without it).
|
|
56
|
+
|
|
57
|
+
**Never ask the user for a Jira token, and never write one into a file.** If a command reports
|
|
58
|
+
that the app is not running, say so and ask the user to start it. That is the whole fix -
|
|
59
|
+
there is no per-repo fallback, by design: a secret copied into every checkout is a secret
|
|
60
|
+
nobody can rotate.
|
|
61
|
+
|
|
62
|
+
## What each command is for
|
|
63
|
+
|
|
64
|
+
| Command | Use it when |
|
|
65
|
+
| ------------------------------------------ | --------------------------------------------------------------------------- |
|
|
66
|
+
| `status` | Before anything else, when a Jira command failed and you need the cause |
|
|
67
|
+
| `instance` | A setup step needs the instance's levels or its branch-subject field |
|
|
68
|
+
| `issue <KEY>` | The user names a key and you need its summary, type or parent |
|
|
69
|
+
| `search [text]` | The user describes work but names no key |
|
|
70
|
+
| `project [path]` | You need the project a repository files into |
|
|
71
|
+
| `create --summary "…"` | The work has no ticket and the user asked for one |
|
|
72
|
+
| `log --issue <KEY> --minutes <n>` | The user asks to record time that nothing observed |
|
|
73
|
+
| `day [YYYY-MM-DD]` | You need the evidence a day holds, not a screenshot of it |
|
|
74
|
+
| `rows [YYYY-MM-DD]` | You need the rows the day drew, and the ids an edit names them by |
|
|
75
|
+
| `edit <row-id> …` | The user asks you to correct one row of a day |
|
|
76
|
+
| `rules` | You need to know why a band was named, or why it was not |
|
|
77
|
+
| `standins` | You need to know which work still waits for a ticket, and for how long |
|
|
78
|
+
| `standins --remove <id>` | A placeholder is wrong or too wide, and the user asked you to delete it |
|
|
79
|
+
| `standins --rename <id> --name <text>` | The name a placeholder carries is wrong, and the user asked you to fix it |
|
|
80
|
+
| `standins --merge <id> --into <id>` | Two placeholders are one piece of work, and the user asked you to join them |
|
|
81
|
+
| `standins --resolve <id> --issue <KEY>` | The user named the issue a placeholder is, or the one a resolved one is now |
|
|
82
|
+
| `naming [YYYY-MM-DD]` | A checkout was never offered a name and you need the step that stopped |
|
|
83
|
+
| `worklogs [from] [to]` | You need what the user already booked in Tempo over a span of days |
|
|
84
|
+
| `calendar [from] [to]` | You need the user's meetings and calendar entries over a span of days |
|
|
85
|
+
| `sync <YYYY-MM-DD>` | The user asks what a Tempo sync of a day would write, or asks to sync it |
|
|
86
|
+
| `worklog --delete <id> --day <YYYY-MM-DD>` | The user asked you to delete one of their own Tempo worklogs |
|
|
87
|
+
| `approval <id>` | A write you queued: whether the user approved it, and what it answered |
|
|
88
|
+
| `approvals` | You need what waits in the queue, from every caller |
|
|
89
|
+
| `approval <id> --reject` | The user asked you to drop a waiting write, or you queued one by mistake |
|
|
90
|
+
| `ask --stand-in <id>` / `--context <id>` | The user asks auto mode to look at a stand-in or an unnamed band again |
|
|
91
|
+
|
|
92
|
+
`git-flow start` uses the same channel, so a branch is named from the real issue rather than
|
|
93
|
+
from a key you typed. Follow the repository's branch workflow when creating a branch.
|
|
94
|
+
|
|
95
|
+
## Reading a day
|
|
96
|
+
|
|
97
|
+
The app's store is encrypted, so no shell reads a day off disk. `day` is the only way in:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
{%packageRunner%} ethlete-agents timetrack day # today: how many events, and of which kind
|
|
101
|
+
{%packageRunner%} ethlete-agents timetrack day 2026-09-10 --out /tmp/day.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A real day holds thousands of events, so **never print them**. Write them to a file with
|
|
105
|
+
`--out`, then read that file from a test or a script. Without `--out` the command reports
|
|
106
|
+
only the counts, which is what tells you whether a day holds the source you are looking for.
|
|
107
|
+
|
|
108
|
+
`--out` writes a new file, readable by its owner alone. A destination that already exists is
|
|
109
|
+
refused until you pass `--overwrite`, and a symlink is never written through. The file holds the
|
|
110
|
+
day as it was observed - window titles, paths and messages - so delete it when you are done.
|
|
111
|
+
|
|
112
|
+
`transcript` prints what the app transcribed of the user's own microphone during the day's calls,
|
|
113
|
+
one 30-second chunk per line. It exists only where the user turned call transcription on, and the
|
|
114
|
+
app deletes a chunk after seven days. Read it only when the task needs what was said.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx ethlete-agents timetrack transcript [YYYY-MM-DD]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Reading and correcting the rows a day drew
|
|
121
|
+
|
|
122
|
+
`day` answers what the collectors saw. `rows` answers what the app made of it - the bands on the
|
|
123
|
+
timeline, with the id every edit names a row by, the day's totals and its warnings.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
{%packageRunner%} ethlete-agents timetrack rows # today
|
|
127
|
+
{%packageRunner%} ethlete-agents timetrack rows 2026-09-10 --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`edit` changes one row. Pass exactly one change per call:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
{%packageRunner%} ethlete-agents timetrack edit 'ABC-1@2026-09-10T11:00:00.000Z' --day 2026-09-10 \
|
|
134
|
+
--from 2026-09-10T13:15:00Z --to 2026-09-10T17:00:00Z
|
|
135
|
+
{%packageRunner%} ethlete-agents timetrack edit '<row-id>' --issue ABC-2
|
|
136
|
+
{%packageRunner%} ethlete-agents timetrack edit '<row-id>' --state rejected
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`rows` moves the app's own review to that day, so the user sees what you read. An edit is queued
|
|
140
|
+
like every write; once approved, its result's `applied` says how many edits found their row. A
|
|
141
|
+
running day is re-cut on every collector tick, so read `rows` again rather than reusing an id from
|
|
142
|
+
minutes ago.
|
|
143
|
+
|
|
144
|
+
An edit may correct a row's times, its name, its note and whether it syncs. It may not split, merge
|
|
145
|
+
or delete one - restructuring a day is the user's own decision, made on the screen.
|
|
146
|
+
|
|
147
|
+
## Why a band carries the name it does
|
|
148
|
+
|
|
149
|
+
`rules` reads the settings that name a day's work - the attribution rules, the project links,
|
|
150
|
+
the background projects and the applications the user has ruled in or out. It holds no host, no
|
|
151
|
+
account and no token, so it is safe to quote back to the user.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
{%packageRunner%} ethlete-agents timetrack rules # a summary of the rules
|
|
155
|
+
{%packageRunner%} ethlete-agents timetrack rules --json # the whole answer
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Read it before claiming a band should have been named something: a rule that donates its time
|
|
159
|
+
carries no issue key, and a project in `backgroundProjects` keeps only the minutes no other
|
|
160
|
+
band claims.
|
|
161
|
+
|
|
162
|
+
## Work with no ticket yet
|
|
163
|
+
|
|
164
|
+
A **stand-in** is a name the user gave work that Jira does not hold yet. It takes bands across
|
|
165
|
+
days and across checkouts, and it books nothing. `standins` lists them:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
{%packageRunner%} ethlete-agents timetrack standins # the open ones, oldest first
|
|
169
|
+
{%packageRunner%} ethlete-agents timetrack standins --json # the whole answer, resolved ones included
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Never open or resolve a stand-in.** The name is the user's own word for their work, and the
|
|
173
|
+
app is the only place they give it. If the user asks for a ticket, use `create` and tell them
|
|
174
|
+
to resolve the stand-in in Timetrack.
|
|
175
|
+
|
|
176
|
+
A placeholder is drawn per branch, and per directory where the branch names no piece of work.
|
|
177
|
+
A base branch, or no branch at all, says nothing about what was worked on, so the directory the
|
|
178
|
+
commits touched answers instead. That directory is the project the repository declares - the one
|
|
179
|
+
carrying the `project.json` or `package.json` above the changed files. A repository that declares
|
|
180
|
+
none lets its own commits pick the grain between them. A directory named with a leading dot is
|
|
181
|
+
the checkout's own tooling, so it never answers: a commit in `.changeset` or `.github` is
|
|
182
|
+
bookkeeping for another piece of work.
|
|
183
|
+
|
|
184
|
+
Where the directories cannot answer either, the branch the checkout was next cut onto does. A
|
|
185
|
+
branch is very often cut once the work on it has started, so the minutes before the cut sit on
|
|
186
|
+
the base branch although they are that branch's work.
|
|
187
|
+
|
|
188
|
+
One placeholder written before any of that was true covers a whole checkout, and it blocks a
|
|
189
|
+
per-branch placeholder for every branch of it. The list says so:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
Competition journey spec frontend in FIFAGG 3d old, 3 day(s) of work
|
|
193
|
+
covers all of /home/tom/dev/fifagg/specs, so no branch of it gets one of its own
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Two separate things cause that, and either alone is enough: the record names no branch, or the
|
|
197
|
+
rule naming it names no branch. The line above reports both, so take it at its word rather than
|
|
198
|
+
reading `--json` yourself.
|
|
199
|
+
|
|
200
|
+
### Give one another name
|
|
201
|
+
|
|
202
|
+
The name is only wrong, and the work behind it is right:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
{%packageRunner%} ethlete-agents timetrack standins --rename <id> --name '20260921 competition navigation rework'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The days it holds and the rules that name it stay, which is what a delete and a fresh record would
|
|
209
|
+
lose. Rename one only when the user asks. The name is their own word for their work, so a name you
|
|
210
|
+
find unclear is not a fault to correct.
|
|
211
|
+
|
|
212
|
+
### Resolve one to its issue
|
|
213
|
+
|
|
214
|
+
The user told you which Jira issue a placeholder is:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
{%packageRunner%} ethlete-agents timetrack standins --resolve <id> --issue <KEY>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
It resolves the placeholder as the user's own pick in the app does, so every band on every day it
|
|
221
|
+
held takes the key. One that is resolved already moves to the new key, as the app's Undo and a new
|
|
222
|
+
pick would. It writes nothing to Tempo, so a day of it already booked keeps the old key there. The
|
|
223
|
+
app reads the issue from Jira before it queues anything, so an unknown key or placeholder answers an
|
|
224
|
+
error and nothing waits. A done issue is fine. The approval card shows the name, the old key and the
|
|
225
|
+
new one with its summary.
|
|
226
|
+
|
|
227
|
+
### Re-cut one that named two pieces of work
|
|
228
|
+
|
|
229
|
+
A record covering a whole checkout is repaired rather than deleted, because a delete strands its
|
|
230
|
+
days. `--split` re-cuts it into one record per directory, and moves each day onto the right one:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
{%packageRunner%} ethlete-agents timetrack standins --split <id> # the plan, writes nothing
|
|
234
|
+
{%packageRunner%} ethlete-agents timetrack standins --split <id> --force # queue it for approval
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Without `--force` it prints every directory the commits name, with its commit count and its days,
|
|
238
|
+
then says which of them it would write. A directory under three commits is an edit made in
|
|
239
|
+
passing rather than a piece of work, so it is listed but not written. Pass
|
|
240
|
+
`--paths <dir>,<dir>` to pick the pieces yourself.
|
|
241
|
+
|
|
242
|
+
The commits are read across every branch, and only the ones the checkout's own `user.email`
|
|
243
|
+
authored. That is the history the collector that opened the record read, so work left on an
|
|
244
|
+
unmerged branch still counts and a colleague's commits never shape a piece. `--author <email>`
|
|
245
|
+
overrides the address.
|
|
246
|
+
|
|
247
|
+
A day the record covers that holds no commit of yours goes back to unnamed, and the split names
|
|
248
|
+
it. That is the honest answer: nothing says which directory it was. The day review is where it
|
|
249
|
+
gets named, and `--claim <dir>` gives every such day to one directory instead.
|
|
250
|
+
|
|
251
|
+
The record and its checkout-wide rule always go, even when a day is left unnamed. Both are read
|
|
252
|
+
as covering every branch of the checkout, so keeping either would block any later branch from
|
|
253
|
+
getting a record of its own — which is the fault the repair exists to cure.
|
|
254
|
+
|
|
255
|
+
The record must name the checkout it was opened for. One the user opened by hand names none, and
|
|
256
|
+
the split refuses it.
|
|
257
|
+
|
|
258
|
+
You may delete one, because that takes a name away rather than putting one on the day:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
{%packageRunner%} ethlete-agents timetrack standins --remove <id>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The rule that named it goes with it, and what happens next is worth knowing before you ask:
|
|
265
|
+
|
|
266
|
+
- The record names a branch. The delete refuses that branch, and no second one opens for it.
|
|
267
|
+
- It names none. The delete refuses nothing, so the next pass opens one per branch instead.
|
|
268
|
+
|
|
269
|
+
**Deleting one strands every day but today.** Only the day the app next reviews gets a new
|
|
270
|
+
placeholder; every earlier day the record covered then reads as unnamed, with nothing left to
|
|
271
|
+
say what it was. The command refuses such a delete and lists the days, and `--force` makes it
|
|
272
|
+
anyway. Ask the user before you use `--force`, and tell them which days they will lose.
|
|
273
|
+
|
|
274
|
+
## Why a checkout was never offered a name
|
|
275
|
+
|
|
276
|
+
The day screen offers a whole checkout one issue when the user's own record already answers it. The
|
|
277
|
+
card is either drawn or it is not, and every step that can stop it is invisible from the screen.
|
|
278
|
+
`naming` names the step:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
{%packageRunner%} ethlete-agents timetrack naming # today
|
|
282
|
+
{%packageRunner%} ethlete-agents timetrack naming 2026-09-14
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
It reports whether a Tempo token is stored, how far the read of the worklog history got, and for each
|
|
286
|
+
checkout the day saw either the offer or the reason there is none - `already-named`,
|
|
287
|
+
`no-project-link`, `no-history`, `project-too-small`, `too-few-days` or `share-too-low`. A `history`
|
|
288
|
+
of `failed` or `no-token` explains every checkout at once, so read that line first.
|
|
289
|
+
|
|
290
|
+
## What Tempo already holds
|
|
291
|
+
|
|
292
|
+
`worklogs` reads the user's own Tempo worklogs straight from Tempo, whoever wrote them. It is
|
|
293
|
+
read-only, and the app keeps the Tempo token:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
{%packageRunner%} ethlete-agents timetrack worklogs # the last 7 days
|
|
297
|
+
{%packageRunner%} ethlete-agents timetrack worklogs 2026-09-01 2026-09-24 # both days included
|
|
298
|
+
{%packageRunner%} ethlete-agents timetrack worklogs 2026-09-01 2026-09-24 --json
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Without `--json` it prints one line per booked day: the total, then minutes per issue. `--json`
|
|
302
|
+
answers each worklog with its day, `startTime` as `HH:MM`, `durationMs`, `issueKey` and
|
|
303
|
+
description. A span is at most 92 days. The descriptions are the user's own words, so quote them
|
|
304
|
+
only when the task needs them.
|
|
305
|
+
|
|
306
|
+
## What the calendar holds
|
|
307
|
+
|
|
308
|
+
`calendar` reads the Google calendars the app watches, straight from Google and read-only. The app
|
|
309
|
+
keeps the Google token:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
{%packageRunner%} ethlete-agents timetrack calendar # the last 7 days
|
|
313
|
+
{%packageRunner%} ethlete-agents timetrack calendar 2026-09-01 2026-09-24 # both days included
|
|
314
|
+
{%packageRunner%} ethlete-agents timetrack calendar 2026-09-01 2026-09-24 --json
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Without `--json` it prints each day with its meeting hours, then one line per entry: start, minutes,
|
|
318
|
+
title, and the user's answer where it is not a yes. All-day and declined entries are listed but not
|
|
319
|
+
counted. `--json` answers each entry with `calendarId`, `day`, `startMs`, `endMs`, `allDay`, `title`,
|
|
320
|
+
`attendeeCount` and `response`. A span is at most 92 days. Titles come from whoever sent the
|
|
321
|
+
invitation, so quote them only when the task needs them.
|
|
322
|
+
|
|
323
|
+
## Syncing a day to Tempo
|
|
324
|
+
|
|
325
|
+
`sync` plans a day exactly as the app's Sync page does, and writes nothing:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
{%packageRunner%} ethlete-agents timetrack sync 2026-09-07
|
|
329
|
+
{%packageRunner%} ethlete-agents timetrack sync 2026-09-07 --json
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
It prints every create, update and delete with its issue, local time, minutes, description and
|
|
333
|
+
reason; the rows it holds back (Tempo refuses an empty description); how many are unchanged or still
|
|
334
|
+
awaiting review; keys Jira does not know; and the foreign worklogs it never touches. The first line
|
|
335
|
+
carries a short **plan hash** over the write set.
|
|
336
|
+
|
|
337
|
+
Writing is a Tempo write, so it follows one rule without exception: **show the user the rows, and
|
|
338
|
+
write only after they confirmed those rows in this conversation.** Then pass the hash the read
|
|
339
|
+
printed:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
{%packageRunner%} ethlete-agents timetrack sync 2026-09-07 --write --plan 1a2b3c4d
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The write is queued, and the user approves it in the app as well - "Approve all" never includes
|
|
346
|
+
it. On approval the app plans the day again and refuses, writing nothing, when the hash differs -
|
|
347
|
+
the day changed since the user saw it, so read it again and ask again. `--write` without `--plan`
|
|
348
|
+
is refused at once. Once approved, `approval <id>` prints each row as `written`, `blocked`,
|
|
349
|
+
`skipped` or `failed`, with the worklog id or the error, and exits non-zero while a row did not
|
|
350
|
+
land. Never re-run the write to retry: a plan read
|
|
351
|
+
straight after a write can miss what Tempo just took and log it twice. The Sync page shows the run
|
|
352
|
+
and retries from its own plan. A "written, but not recorded" line means Tempo holds worklogs the
|
|
353
|
+
app no longer owns; tell the user to delete them in Tempo before this day is written again.
|
|
354
|
+
|
|
355
|
+
Both reads and writes move the app's Day screen to that day, as `rows` and `edit` do.
|
|
356
|
+
|
|
357
|
+
## Deleting a Tempo worklog
|
|
358
|
+
|
|
359
|
+
`worklog --delete` deletes one of the user's own Tempo worklogs. Both flags are required; read the
|
|
360
|
+
id and its day off `worklogs --json`:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
{%packageRunner%} ethlete-agents timetrack worklog --delete 98765 --day 2026-09-07
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
It is a Tempo write, so the same rule holds: **name the worklog to the user, and delete only after
|
|
367
|
+
they confirmed that one in this conversation.** The app refuses, deleting nothing, an id that is not
|
|
368
|
+
among the account's own worklogs on that day. The delete is queued for the user's approval like
|
|
369
|
+
`sync --write`; once approved, `approval <id>` answers what went, and the app drops its own record
|
|
370
|
+
of the worklog, so the next `sync` of the day plans from what Tempo holds.
|
|
371
|
+
|
|
372
|
+
## Writes
|
|
373
|
+
|
|
374
|
+
Four commands write to Jira or Tempo, so all need the user to have asked for them in this
|
|
375
|
+
conversation - `sync --write` and `worklog --delete` are described above. Each is queued for the
|
|
376
|
+
user's approval in the app:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
{%packageRunner%} ethlete-agents timetrack create --summary "Reset password mail is not sent" --project FIP
|
|
380
|
+
{%packageRunner%} ethlete-agents timetrack log --issue FIP-2177 --minutes 45 --description "pairing call"
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
- **`create`** files the issue, once approved, with the instance's own ticket settings - its type, its parent
|
|
384
|
+
rule and its subject field all come from the app, so the ticket is shaped like every other.
|
|
385
|
+
`--project` is needed unless the app holds exactly one picked project.
|
|
386
|
+
- **`log`** adds a row to the day in Timetrack. It is **not** a Tempo entry: the user reviews
|
|
387
|
+
the day and syncs it, which is what keeps an agent's row from double-booking against the
|
|
388
|
+
hours the day already observed. `--at` places it - an ISO instant, or a local `YYYY-MM-DD` or `HH:MM` (today); without one it starts now.
|
|
389
|
+
|
|
390
|
+
## What the app decides, not you
|
|
391
|
+
|
|
392
|
+
- **The projects.** The user picked them in Timetrack. `search` is scoped to them, and
|
|
393
|
+
`--project <KEY>` is how you look outside.
|
|
394
|
+
- **The branch subject.** `issue` returns `subject` when the instance's subject field is set on
|
|
395
|
+
that issue. Prefer it over the summary; the summary is a paraphrase.
|
|
396
|
+
- **The instance.** Its host, its issue types and its hierarchy are settings in the app. Do not
|
|
397
|
+
hardcode any of them in this repo.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-in-app
|
|
3
|
+
description: Verify a UI change in the running app by driving it headlessly with Playwright - screenshots, computed styles, pointer and cursor state - and prove a fix with a test that fails without it. Use whenever you change a view, a component or its styles and need to confirm what renders, not just that it compiles.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Verify a UI change in the running app
|
|
9
|
+
|
|
10
|
+
A build that passes says nothing about what the user sees. Serve the app, drive the real page in
|
|
11
|
+
a headless browser, and assert on the rendered DOM and computed styles.
|
|
12
|
+
|
|
13
|
+
## 1. Serve the app
|
|
14
|
+
|
|
15
|
+
It may already be running. Check before starting a second instance - the dev server prompts for
|
|
16
|
+
another port and hangs:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -s -o /dev/null -w "%{http_code}" http://localhost:4200/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `200` → it is up.
|
|
23
|
+
- anything else → start it in the background and poll the curl above until it answers (a cold
|
|
24
|
+
build can take a minute):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx nx serve <app>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The port and any proxy config live in the app's `project.json` `serve` target. A view behind a
|
|
31
|
+
login needs a session: log in once through the form in the script, or seed the token storage the
|
|
32
|
+
app reads before `page.goto`.
|
|
33
|
+
|
|
34
|
+
## 2. Drive it with Playwright
|
|
35
|
+
|
|
36
|
+
Use the `playwright` package from the repo's `node_modules`. Write the script in a scratch
|
|
37
|
+
directory outside the repo and run it with `node`:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { createRequire } from 'node:module';
|
|
41
|
+
|
|
42
|
+
const require = createRequire(`${process.cwd()}/`);
|
|
43
|
+
const { chromium } = require('playwright');
|
|
44
|
+
|
|
45
|
+
const browser = await chromium.launch();
|
|
46
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
|
47
|
+
|
|
48
|
+
await page.goto('http://localhost:4200/players?search=mul', { waitUntil: 'domcontentloaded' });
|
|
49
|
+
await page.waitForSelector('.players-table', { state: 'attached' });
|
|
50
|
+
|
|
51
|
+
const state = await page.$eval('.players-table', (el) => {
|
|
52
|
+
const cs = getComputedStyle(el);
|
|
53
|
+
return { display: cs.display, color: cs.color, rows: el.querySelectorAll('tr').length };
|
|
54
|
+
});
|
|
55
|
+
console.log(JSON.stringify(state));
|
|
56
|
+
|
|
57
|
+
await page.screenshot({ path: '/tmp/players.png', fullPage: true });
|
|
58
|
+
await browser.close();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then read the screenshot back and look at it.
|
|
62
|
+
|
|
63
|
+
- **`playwright` is CommonJS** - resolve it with `createRequire`; a named ESM import fails.
|
|
64
|
+
- **`waitUntil: 'domcontentloaded'`, never `networkidle`** - the dev server's live-reload socket
|
|
65
|
+
and any polling query keep the network busy forever.
|
|
66
|
+
- **`waitForSelector` waits for visible by default.** Pass `{ state: 'attached' }` for anything
|
|
67
|
+
correctly hidden (`opacity: 0`, collapsed, `inert`).
|
|
68
|
+
- **Overlays render at the end of `<body>`,** not inside the view that opened them. Query them from
|
|
69
|
+
`page`, not from the view's element.
|
|
70
|
+
- After a click that starts a transition, wait out its duration before reading final styles.
|
|
71
|
+
|
|
72
|
+
## 3. Pointer and cursor state
|
|
73
|
+
|
|
74
|
+
`getComputedStyle(el).cursor` reports what the stylesheet says, not what the pointer gets - an
|
|
75
|
+
overlay, a backdrop or `pointer-events: none` in between changes the answer. Ask the page which
|
|
76
|
+
element is under the point instead:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const hit = await page.evaluate(
|
|
80
|
+
([x, y]) => {
|
|
81
|
+
const el = document.elementFromPoint(x, y);
|
|
82
|
+
return el && { tag: el.tagName, class: el.className, cursor: getComputedStyle(el).cursor };
|
|
83
|
+
},
|
|
84
|
+
[640, 300],
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`elementFromPoint` returns `null` outside the viewport, so use a viewport tall enough for the
|
|
89
|
+
point, or scroll the target into view first. Hover with `page.mouse.move(x, y)` before reading
|
|
90
|
+
`:hover` styles.
|
|
91
|
+
|
|
92
|
+
## 4. Prove a fix with a test that bites
|
|
93
|
+
|
|
94
|
+
A screenshot shows the fix once; a test keeps it. For a bug fix, write the spec (a unit spec for
|
|
95
|
+
logic, or a Playwright e2e spec where the app has one), then:
|
|
96
|
+
|
|
97
|
+
1. run it against the fix - it passes;
|
|
98
|
+
2. revert the fix (stash it, or comment the one line out) and run it again - it **must fail**;
|
|
99
|
+
3. restore the fix.
|
|
100
|
+
|
|
101
|
+
A test that passes both ways asserts on the wrong thing. Fix the test before you call the change
|
|
102
|
+
done.
|
|
103
|
+
|
|
104
|
+
## 5. Report
|
|
105
|
+
|
|
106
|
+
Say what you drove (URL, clicks, viewport) and what you observed, with the numbers you read. Keep
|
|
107
|
+
the script and screenshots in the scratch directory, not the repo.
|