@ethlete/agent-rules 0.1.0-next.2 → 0.1.0-next.20
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/CHANGELOG.md +269 -0
- package/README.md +350 -29
- package/THIRD-PARTY-LICENSES.md +35 -0
- package/content/git-hooks/post-checkout.sh +10 -0
- package/content/git-hooks/pre-push.sh +5 -0
- package/content/hooks/context-warning.py +766 -108
- package/content/hooks/subagent-model-policy.py +187 -0
- package/content/output-styles/ste-clarity.md +131 -0
- package/content/rules/app-styling.md +59 -0
- package/content/rules/comments.md +53 -20
- package/content/rules/lint-and-format.md +6 -5
- package/content/rules/nx-layout.md +47 -0
- package/content/rules/reactive-state.md +1 -1
- package/content/rules/styling.md +9 -7
- package/content/rules/subagent-models.md +29 -0
- package/content/skills/angular-patterns/SKILL.md +30 -7
- package/content/skills/api-source/SKILL.md +130 -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 +205 -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/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 +6 -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 +34 -0
- package/content/skills/handoff/SKILL.md +74 -4
- package/content/skills/query/SKILL.md +165 -64
- package/content/skills/rxjs-signals/SKILL.md +31 -4
- package/content/skills/sdk-docs/SKILL.md +50 -14
- package/content/skills/sdk-local-build/SKILL.md +30 -10
- package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
- package/content/skills/sdk-source/SKILL.md +49 -21
- package/content/skills/sdk-update/SKILL.md +96 -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 +41 -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 +16 -2
- package/src/index.js +63 -28
- 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 +38 -4
- package/src/lib/config.js +84 -8
- 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 +10 -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 +157 -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 +157 -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 +132 -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 +108 -0
- package/src/lib/gitlab.js.map +1 -0
- package/src/lib/index.d.ts +2 -0
- package/src/lib/index.js +2 -0
- package/src/lib/index.js.map +1 -1
- package/src/lib/migrate.js +1 -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 +3 -0
- package/src/lib/package-runner.js +69 -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 +147 -40
- package/src/lib/plan.js.map +1 -1
- package/src/lib/prompt.d.ts +8 -0
- package/src/lib/prompt.js +27 -0
- package/src/lib/prompt.js.map +1 -0
- package/src/lib/render.d.ts +24 -2
- package/src/lib/render.js +39 -10
- package/src/lib/render.js.map +1 -1
- package/src/lib/sync.d.ts +0 -1
- package/src/lib/sync.js +23 -15
- 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 +1 -23
- package/src/lib/targets/claude-hooks.js +17 -85
- package/src/lib/targets/claude-hooks.js.map +1 -1
- package/src/lib/targets/claude.js +1 -2
- 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 +34 -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 +3 -3
- package/src/lib/targets/copilot.js.map +1 -1
- package/src/lib/targets/cursor.js +2 -2
- package/src/lib/targets/cursor.js.map +1 -1
- package/src/lib/targets/git-hooks.d.ts +24 -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 +58 -0
- package/src/lib/targets/hooks-shared.js +128 -0
- package/src/lib/targets/hooks-shared.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 +1055 -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 +271 -0
- package/src/lib/timetrack.js.map +1 -0
- package/content/skills/styleguide/STYLEGUIDE.md +0 -520
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: angular-patterns
|
|
3
|
-
description: How to build Angular pieces the Ethlete way - templates, lifecycle, and when to reach for a component/directive/service/pipe vs a plain function. Read when writing or restructuring a component, directive, service, or pipe, wiring up lifecycle, or binding values in a template.
|
|
3
|
+
description: How to build Angular pieces the Ethlete way - templates, lifecycle, and when to reach for a component/directive/service/pipe vs a plain function. Read when writing or restructuring a component, directive, service, or pipe, wiring up lifecycle, or binding values in a template.
|
|
4
4
|
kind: skill
|
|
5
5
|
scope: both
|
|
6
6
|
requires: ['@ethlete/core']
|
|
@@ -22,10 +22,20 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
|
22
22
|
|
|
23
23
|
```html
|
|
24
24
|
<!-- ❌ runs every CD cycle -->
|
|
25
|
-
<button [disabled]="isDisabled()">
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
<button [disabled]="isDisabled()"></button>
|
|
26
|
+
|
|
27
|
+
<!-- ✅ computed signal -->
|
|
28
|
+
<button [disabled]="disabled()"></button>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **Per-row formatting belongs in the data, not in a method per row.** Map the rows once
|
|
32
|
+
in a `computed()` and bind the prepared fields. A pure, module-level formatting function
|
|
33
|
+
is acceptable too; a component method called for each row in `@for` is not.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// ❌ <td>{{ formatDate(row.createdAt) }}</td> - re-runs for every row, every CD cycle
|
|
37
|
+
// ✅
|
|
38
|
+
rows = computed(() => this.items().map((item) => ({ ...item, createdAtLabel: formatDate(item.createdAt) })));
|
|
29
39
|
```
|
|
30
40
|
|
|
31
41
|
## Lifecycle
|
|
@@ -40,6 +50,10 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
|
40
50
|
`createRootProvider` and the `injectX()` helper pattern from `@ethlete/core`
|
|
41
51
|
rather than an `@Injectable` or `@Service`. (Both decorators are lint-banned;
|
|
42
52
|
choosing a function over a service at all is the judgment.)
|
|
53
|
+
- **Repeated component logic → one `injectX()` function.** When two components carry the
|
|
54
|
+
same signals, effects or subscriptions, extract them into an `injectX()` that runs in the
|
|
55
|
+
injection context and returns what the components bind. Do not copy the block, and do not
|
|
56
|
+
reach for a base class.
|
|
43
57
|
- **Directives → plain functions where possible.** With signal APIs, move the core
|
|
44
58
|
logic into a function so it's reusable without applying a directive; keep a
|
|
45
59
|
directive only when a host element genuinely needs it. Avoid common input/output
|
|
@@ -47,12 +61,21 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
|
47
61
|
- **Pipes → a `computed()` calling a utility function.** Pipes carry no logic;
|
|
48
62
|
most can be dropped in favour of a `computed`.
|
|
49
63
|
|
|
64
|
+
## Query params
|
|
65
|
+
|
|
66
|
+
- **Read** them with the `@ethlete/core` signals: `injectQueryParam(key)`,
|
|
67
|
+
`injectQueryParams()`. `ActivatedRoute` and `router.url` are lint-banned.
|
|
68
|
+
- **Write** them with `inject(Router).navigate([], { queryParams, queryParamsHandling: 'merge' })`.
|
|
69
|
+
`merge` keeps the params you do not set; `null` removes a param.
|
|
70
|
+
- **Filter, sort and page state bound to the URL** is a query form: with `@ethlete/query`,
|
|
71
|
+
use `defineQueryForm` instead of writing the params by hand.
|
|
72
|
+
|
|
50
73
|
## Components
|
|
51
74
|
|
|
52
75
|
- Inline template/styles for small components; external `.html` / `.css` files
|
|
53
76
|
for complex ones.
|
|
54
|
-
- Component CSS is plain CSS wrapped in `@layer components`, with every colour
|
|
55
|
-
|
|
77
|
+
- Component CSS is plain CSS wrapped in `@layer components`, with every primary colour
|
|
78
|
+
value coming from the repository's theme tokens.
|
|
56
79
|
|
|
57
80
|
## Reactive state
|
|
58
81
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-source
|
|
3
|
+
description: How to read the source of the API an app in this repo talks to, from a local backend checkout named in ethlete.config.local.json. Read when a response shape, a status code, an error body, an enum or an auth rule has to be confirmed rather than guessed - and never edit that checkout as part of work in this repo.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reading the API source
|
|
9
|
+
|
|
10
|
+
The client's types describe what the frontend _expects_. When the two disagree, the
|
|
11
|
+
server is right, so a question about the contract is answered in the API repository -
|
|
12
|
+
not by reading the frontend's own models harder.
|
|
13
|
+
|
|
14
|
+
Reach for the checkout when:
|
|
15
|
+
|
|
16
|
+
- a response arrives with a field, a shape or a `null` the frontend types do not allow
|
|
17
|
+
- you need the exact status code, error body or validation message for a failure path
|
|
18
|
+
- an enum, a permission or a filter parameter has to match the server's list exactly
|
|
19
|
+
- a request fails and you cannot tell whether the client or the server is wrong
|
|
20
|
+
- you are about to report or fix something in the API itself
|
|
21
|
+
|
|
22
|
+
Do not read it to design frontend code that the API does not serve yet. An endpoint in
|
|
23
|
+
the checkout is not deployed until the API team ships it.
|
|
24
|
+
|
|
25
|
+
## 1. Resolve the checkout for your app
|
|
26
|
+
|
|
27
|
+
One repository can hold several apps, each with its own API, so the paths are a map from
|
|
28
|
+
app to checkout. They are per machine, so the map lives in the gitignored
|
|
29
|
+
`ethlete.config.local.json` at the repo root:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"apiRepoPaths": {
|
|
34
|
+
"hub": "../fut-hub-backend",
|
|
35
|
+
"*": "../shared-backend"
|
|
36
|
+
},
|
|
37
|
+
"apiRepoBranches": {
|
|
38
|
+
"hub": "develop",
|
|
39
|
+
"*": "main"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Older checkouts still keep these keys in `ethlete-agents.config.local.json`. Read that file as a
|
|
45
|
+
fallback when `ethlete.config.local.json` is absent.
|
|
46
|
+
|
|
47
|
+
Read that file before searching anywhere. The rules:
|
|
48
|
+
|
|
49
|
+
- **The key is the app** as this repo names it - the workspace project name, which is
|
|
50
|
+
normally also the folder under `apps/`. Match the app you are working in.
|
|
51
|
+
- **A relative path resolves from the repo root**, not from the app folder.
|
|
52
|
+
- **Require an exact app key.** If several apps intentionally share one API, configure
|
|
53
|
+
the explicit `"*"` fallback. Never treat an unrelated single entry as a fallback.
|
|
54
|
+
- **No matching entry - stop and ask.** Do not guess a sibling folder and do not clone
|
|
55
|
+
the repository. Say which app you needed the API for and offer the snippet above; the
|
|
56
|
+
file is gitignored, so adding it changes nothing for anyone else.
|
|
57
|
+
|
|
58
|
+
Without a checkout, fall back to what the running API tells you: the generated API
|
|
59
|
+
description if the project serves one (`/openapi.json`, `/swagger`, `/api/doc`), and the
|
|
60
|
+
real response body of the call you are debugging.
|
|
61
|
+
|
|
62
|
+
## 2. Resolve the relevant files before judging checkout state
|
|
63
|
+
|
|
64
|
+
Search by the route, field, or error you already know. Record the contract, serializer,
|
|
65
|
+
handler, and tests that can answer the question. Then inspect checkout state only for
|
|
66
|
+
those paths:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git -C <apiRepoPath> status -sb
|
|
70
|
+
git -C <apiRepoPath> status --short -- <relevant-paths>
|
|
71
|
+
git -C <apiRepoPath> diff -- <relevant-paths>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Unrelated dirty files are not evidence about these paths. Continue without blocking on
|
|
75
|
+
them. If a relevant file is dirty, distinguish the worktree implementation from the
|
|
76
|
+
committed or deployed behavior. Ask only when that difference changes the answer or the
|
|
77
|
+
user has to choose which behavior matters.
|
|
78
|
+
|
|
79
|
+
`apiRepoBranches` configures the expected branch per exact app key, with `"*"` as the
|
|
80
|
+
only fallback. If no branch is configured, report the current branch as context; do not
|
|
81
|
+
invent a blocking “development branch” requirement. A different or ahead branch matters
|
|
82
|
+
only when the relevant files differ in that commit range.
|
|
83
|
+
|
|
84
|
+
Use existing remote refs first. `git fetch` preserves the worktree but is not read-only:
|
|
85
|
+
it uses the network and mutates remote refs. Fetch only when freshness is material, and
|
|
86
|
+
request any approval the environment requires. Never switch, pull, stash, or reset the
|
|
87
|
+
checkout yourself.
|
|
88
|
+
|
|
89
|
+
## 3. The checkout is not the environment the app calls
|
|
90
|
+
|
|
91
|
+
Even a clean, current checkout is only the source. The app talks to a deployed
|
|
92
|
+
environment, which can lag it:
|
|
93
|
+
|
|
94
|
+
- Read the app's API base URL from its environment config, and say which environment
|
|
95
|
+
your answer is about.
|
|
96
|
+
- When the source and the observed response disagree, **the observed response wins** for
|
|
97
|
+
what the app has to handle today. Report the difference instead of writing client code
|
|
98
|
+
against the newer source.
|
|
99
|
+
|
|
100
|
+
## 4. Search it, don't read it whole
|
|
101
|
+
|
|
102
|
+
The stack is whatever the API team chose, so search by the thing you already know - the
|
|
103
|
+
path, the field name, the error message - rather than by an expected file layout:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
rg -n "api/v1/matches" <apiRepoPath> --glob '!*test*' # the route
|
|
107
|
+
rg -n "kickoffAt" <apiRepoPath> # a field of the payload
|
|
108
|
+
rg -n "MATCH_NOT_FOUND" <apiRepoPath> # an error code you received
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Two things to find first, because they answer most questions on their own:
|
|
112
|
+
|
|
113
|
+
- **The API description** the project generates or checks in (OpenAPI, Swagger, GraphQL
|
|
114
|
+
schema, `.http` files). It is the contract, and it is cheaper to read than the code.
|
|
115
|
+
- **The serializer, resource or DTO** for the entity - the field list the client sees,
|
|
116
|
+
which is usually a subset of the database model, and the place where a name is
|
|
117
|
+
rewritten between the two.
|
|
118
|
+
|
|
119
|
+
The API's own tests describe intended behaviour more directly than the implementation:
|
|
120
|
+
they name the status code and the body for each case.
|
|
121
|
+
|
|
122
|
+
## 5. The checkout is read-only from here
|
|
123
|
+
|
|
124
|
+
It is a different repository with its own branch, review and release process. Never edit
|
|
125
|
+
it while working on a task in this repo, and never change its git state without being
|
|
126
|
+
asked. A fetch also needs to meet the freshness and approval conditions above.
|
|
127
|
+
|
|
128
|
+
When the fix belongs in the API, say so precisely: the endpoint, the field, and the
|
|
129
|
+
behaviour it should have. Then handle the API as it is today - a client workaround for a
|
|
130
|
+
server bug is a decision for the user, and it needs a comment naming what it waits on.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-testing
|
|
3
|
+
description: How to unit-test an app built on the Ethlete SDK - views that fetch through @ethlete/query (HttpTestingController and the @ethlete/query/testing helpers), components that open overlays, and views that read the URL with injectQueryParam under provideRouter. Read before writing or fixing a spec for a component, view or service that uses @ethlete/query, @ethlete/core router signals or @ethlete/components overlays.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
requires: ['@ethlete/core']
|
|
7
|
+
vars: [docsBaseUrl]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Testing an Ethlete app
|
|
11
|
+
|
|
12
|
+
Specs run in jsdom under `TestBed`. Test through the public API the view uses - mount the
|
|
13
|
+
component, answer its requests, move the URL - and assert on what it renders or exposes. The full
|
|
14
|
+
reference for the query helpers is {%docsBaseUrl%}/query/testing.
|
|
15
|
+
|
|
16
|
+
## Views that fetch
|
|
17
|
+
|
|
18
|
+
A client made with `createQueryClient` is provided in root, so a spec provides nothing for it. It
|
|
19
|
+
needs Angular's HTTP testing backend, and answers each request by hand:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
23
|
+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
24
|
+
import { TestBed } from '@angular/core/testing';
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => TestBed.inject(HttpTestingController).verify());
|
|
31
|
+
|
|
32
|
+
it('renders the players', async () => {
|
|
33
|
+
const http = TestBed.inject(HttpTestingController);
|
|
34
|
+
const fixture = TestBed.createComponent(PlayersComponent);
|
|
35
|
+
fixture.detectChanges();
|
|
36
|
+
TestBed.tick();
|
|
37
|
+
|
|
38
|
+
http.expectOne((req) => req.url.includes('/players')).flush({ items: ['Müller'] });
|
|
39
|
+
await fixture.whenStable();
|
|
40
|
+
|
|
41
|
+
expect(fixture.nativeElement.textContent).toContain('Müller');
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- A GET query executes once its args resolve, which happens on change detection -
|
|
46
|
+
`TestBed.tick()` before `expectOne`, and again after `flush` before reading the query's
|
|
47
|
+
`response()`, `loading()` or `error()`.
|
|
48
|
+
- `req.url` carries the query string, so `expectOne('https://api.example.com/players?search=mul')`
|
|
49
|
+
matches the full URL. Match with a predicate, as above, when the params are not the point.
|
|
50
|
+
- Fail a request with `flush(body, { status: 422, statusText: 'Unprocessable Entity' })`.
|
|
51
|
+
- Mutations (`POST`, `PUT`, `PATCH`, `DELETE`) never auto-execute - call `.execute({ args })`,
|
|
52
|
+
then expect the request.
|
|
53
|
+
|
|
54
|
+
From `@ethlete/query/testing`:
|
|
55
|
+
|
|
56
|
+
| Export | Use |
|
|
57
|
+
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `expectAndFlush(http, url, body, status?)` | `expectOne(url).flush(body)` in one call. |
|
|
59
|
+
| `expectFlushAndWait(http, url, body, status?)` | The same, then `TestBed.tick()`. |
|
|
60
|
+
| `setupQueryTest(config?)` | A scratch client, `HttpTestingController`, `createGet` … `createDelete` creators - for testing code that takes a creator or client. |
|
|
61
|
+
| `setupAuthTest({ querySetup })` | A bearer auth provider on that scratch client, with `login()` / `refresh()` helpers that answer the requests. |
|
|
62
|
+
| `createFakeQueryPersistenceStore()` | In-memory persistence adapter - jsdom has no IndexedDB. |
|
|
63
|
+
| `installFakeBroadcastChannel()`, `installFakeWebLocks()`, `flushMultiTabSync` | Two clients in one spec behave as two tabs. |
|
|
64
|
+
| `createWebSocketTestDouble()` | A scripted socket.io `io` for `createWebSocketClient({ io })`. |
|
|
65
|
+
|
|
66
|
+
`setupQueryTest` calls `TestBed.configureTestingModule` itself and replaces `ErrorHandler` with a
|
|
67
|
+
no-op (`mockErrorHandler: false` keeps the real one). Call it inside `beforeEach`, and call
|
|
68
|
+
`restoreConsole()` in `afterEach` if the file spies on `console`.
|
|
69
|
+
|
|
70
|
+
## Views that read the URL
|
|
71
|
+
|
|
72
|
+
`injectQueryParam`, `injectQueryParams`, `injectPathParam` and the other router signals from
|
|
73
|
+
`@ethlete/core` read the router, so the spec provides one and navigates it:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { provideRouter, Router } from '@angular/router';
|
|
77
|
+
|
|
78
|
+
TestBed.configureTestingModule({
|
|
79
|
+
providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await TestBed.inject(Router).navigateByUrl('/?search=mul');
|
|
83
|
+
const fixture = TestBed.createComponent(PlayersComponent);
|
|
84
|
+
fixture.detectChanges();
|
|
85
|
+
TestBed.tick();
|
|
86
|
+
|
|
87
|
+
http.expectOne((req) => req.urlWithParams.includes('search=mul')).flush({ items: [] });
|
|
88
|
+
|
|
89
|
+
await TestBed.inject(Router).navigateByUrl('/?search=x');
|
|
90
|
+
TestBed.tick();
|
|
91
|
+
http.expectOne((req) => req.urlWithParams.includes('search=x')).flush({ items: [] });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Navigate to change a param - never set the signal or mock the inject function. A view that also
|
|
95
|
+
reads path params needs the real route in the config (`provideRouter([{ path: 'players/:id',
|
|
96
|
+
component: PlayersComponent }])`) and `RouterTestingHarness` from `@angular/router/testing` to
|
|
97
|
+
render it.
|
|
98
|
+
|
|
99
|
+
## Components that open overlays
|
|
100
|
+
|
|
101
|
+
An overlay mounts at the end of `document.body`, not in the fixture, and it opens and closes over
|
|
102
|
+
animation frames. Query the document, wait two frames, and close what is left after each test:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { injectOverlayManager } from '@ethlete/components';
|
|
106
|
+
|
|
107
|
+
const flushFrames = () =>
|
|
108
|
+
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
109
|
+
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
TestBed.runInInjectionContext(() => injectOverlayManager())
|
|
112
|
+
.openOverlays()
|
|
113
|
+
.forEach((ref) => ref.forceClose());
|
|
114
|
+
await flushFrames();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('opens the edit dialog', async () => {
|
|
118
|
+
const fixture = TestBed.createComponent(PlayerListComponent);
|
|
119
|
+
fixture.detectChanges();
|
|
120
|
+
|
|
121
|
+
fixture.nativeElement.querySelector('button.edit').click();
|
|
122
|
+
await flushFrames();
|
|
123
|
+
|
|
124
|
+
expect(document.querySelector('.player-edit-dialog')).not.toBeNull();
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Count open overlays with `injectOverlayManager().openOverlays().length` (inside
|
|
129
|
+
`TestBed.runInInjectionContext`).
|
|
130
|
+
- The `OverlayRef` that `createOverlayOpener(...).open()` returns has `componentInstance()`,
|
|
131
|
+
`close(result)` and `afterClosed()` - enough to test what the opener does with the result.
|
|
132
|
+
- A query-param overlay (`defineQueryParamOverlay`) opens from the URL: provide the router and
|
|
133
|
+
navigate with the param set, as above.
|
|
134
|
+
- jsdom has no `ResizeObserver`, `IntersectionObserver`, `matchMedia` or `Element.animate`. If a
|
|
135
|
+
spec throws on one of them, stub it once in the test setup file with an inert class or function,
|
|
136
|
+
not per spec.
|
|
137
|
+
|
|
138
|
+
## Before you call it done
|
|
139
|
+
|
|
140
|
+
A spec for a bug fix must fail without the fix - revert it, run the spec, see it fail, restore it.
|
|
141
|
+
To check the same change in the running app, read {%skill:verify-in-app%}.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-subagent
|
|
3
|
+
description: Delegate a task to a Codex agent (gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-6-astra) instead of a Claude subagent - a second opinion from another model family, a review, or a cheap parallel lookup. Read before spawning a subagent when another model would answer better, and whenever the user says codex, GPT, or "second opinion".
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Codex as a subagent
|
|
9
|
+
|
|
10
|
+
`codex exec` runs one Codex agent to completion without a terminal. It is a subagent with its
|
|
11
|
+
own context, its own tools and its own model family. It reads the repository's `AGENTS.md` by
|
|
12
|
+
itself, so it starts with the same rules you have.
|
|
13
|
+
|
|
14
|
+
Use it when the task gains from a model that is not Claude: a second opinion on a design, a
|
|
15
|
+
review of your own diff, a bug hunt that your reading already failed, or work you want to run
|
|
16
|
+
beside your own. Use it also when the Claude usage limit is close, because a Codex run does not
|
|
17
|
+
draw on it - move the delegatable work there and keep the limit for the work you lead. For
|
|
18
|
+
everything else, a Claude subagent stays the cheaper choice.
|
|
19
|
+
|
|
20
|
+
## The call
|
|
21
|
+
|
|
22
|
+
The wrapper is {%resource:codex-agent.mjs%}. It keeps the Codex trace in a log file and prints
|
|
23
|
+
only the final message, so one run costs you a few hundred tokens instead of the whole trace.
|
|
24
|
+
Below, `<skill-dir>` is the directory this file sits in.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node <skill-dir>/codex-agent.mjs -m gpt-5.6-terra -e medium "<prompt>"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Options: `-m` model, `-e` effort (`low`, `medium`, `high`, `xhigh`), `-C` working root,
|
|
31
|
+
`--write`, `--resume <thread>`, `--prompt-file <file>`, `--schema <json-schema-file>`,
|
|
32
|
+
`--timeout <seconds>`, `--trace`. Run it with `--help` for the full list.
|
|
33
|
+
|
|
34
|
+
The run prints a footer with the model, the sandbox, the time, the output tokens, the resume
|
|
35
|
+
id and the log path. Read the log only when a run fails.
|
|
36
|
+
|
|
37
|
+
## Pick the model
|
|
38
|
+
|
|
39
|
+
| Model | The work it fits |
|
|
40
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| `gpt-5.6-luna` | Fast and cheap: a lookup, a grep, a question about one file. |
|
|
42
|
+
| `gpt-5.6-terra` | Balanced agentic coding. The default for real work, and the wrapper's default. |
|
|
43
|
+
| `gpt-5.6-sol` | A reliable workhorse for a straight task. It costs about what `fable` costs, so ask the user before you pick it. |
|
|
44
|
+
| `gpt-6-astra` | Complex, demanding work. The most expensive of the four, so ask the user before you pick it. |
|
|
45
|
+
|
|
46
|
+
Effort follows both the flag and the prompt. Pass `-e low` for a lookup and `-e high` for a
|
|
47
|
+
hard bug. Say "keep it brief" in the prompt when you want a short answer.
|
|
48
|
+
|
|
49
|
+
## Read-only by default
|
|
50
|
+
|
|
51
|
+
A run without `--write` gets the `read-only` sandbox. Codex can read and run commands, but it
|
|
52
|
+
cannot change a file. This is the right mode for a review, a second opinion and research.
|
|
53
|
+
|
|
54
|
+
Pass `--write` only when the user asked for a code change from Codex. Two rules hold then:
|
|
55
|
+
|
|
56
|
+
1. **Codex must not commit, push, or run `git add`.** Say so in the prompt. Other sessions
|
|
57
|
+
share this checkout, so a broad stage takes work that is not yours.
|
|
58
|
+
2. **Name the files it may touch.** An open brief in a shared checkout is how two agents edit
|
|
59
|
+
the same file.
|
|
60
|
+
|
|
61
|
+
`--resume` carries the sandbox and the working root of the thread it resumes. A thread that
|
|
62
|
+
started read-only stays read-only, so start a new run when you need write access.
|
|
63
|
+
|
|
64
|
+
## Run several at once
|
|
65
|
+
|
|
66
|
+
The wrapper blocks until the agent is done, which takes minutes for real work. Start each run
|
|
67
|
+
as a background command, and collect the answers when they arrive. Give each run its own
|
|
68
|
+
question. Two agents on one question waste a model.
|
|
69
|
+
|
|
70
|
+
## Write the prompt for a stranger
|
|
71
|
+
|
|
72
|
+
The Codex agent shares no context with you. It sees your prompt, the repository and
|
|
73
|
+
`AGENTS.md`. So:
|
|
74
|
+
|
|
75
|
+
- State the task as one question, and name what "done" means.
|
|
76
|
+
- Give the file paths you already know. It costs a model minutes to find what you can name.
|
|
77
|
+
- Ask for the shape of the answer: a verdict, a list of findings with `file:line`, a patch in
|
|
78
|
+
a fenced block.
|
|
79
|
+
- Do not paste the repository rules. It reads them itself.
|
|
80
|
+
|
|
81
|
+
Pass `--schema` with a JSON Schema file when you want the answer as JSON you can parse.
|
|
82
|
+
|
|
83
|
+
## Before you reach for it
|
|
84
|
+
|
|
85
|
+
- `codex` must be on `PATH` and logged in. If it is not, tell the user and stop. Do not
|
|
86
|
+
install it, and do not write a token into the repository.
|
|
87
|
+
- If Codex reports that the project is not trusted, ask the user to run `codex` once in the
|
|
88
|
+
repository and trust it there.
|
|
89
|
+
- A Codex run is not free. Prefer it where a second model earns its cost, not for the
|
|
90
|
+
twentieth grep.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run one Codex agent to completion and print only its final message.
|
|
4
|
+
*
|
|
5
|
+
* `codex exec` streams its whole reasoning and command trace to stdout. That trace costs a
|
|
6
|
+
* calling agent more context than the answer is worth, so it stays in a log file here.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node codex-agent.mjs [options] "<prompt>"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { createWriteStream, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_MODEL = 'gpt-5.6-terra';
|
|
18
|
+
const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
19
|
+
|
|
20
|
+
const USAGE = `Usage: node codex-agent.mjs [options] "<prompt>"
|
|
21
|
+
|
|
22
|
+
-m, --model <id> Model id (default ${DEFAULT_MODEL})
|
|
23
|
+
-e, --effort <level> Reasoning effort: low | medium | high | xhigh
|
|
24
|
+
-w, --write Let the agent edit the working tree (default: read-only)
|
|
25
|
+
-C, --cd <dir> Working root for the agent (default: the current directory)
|
|
26
|
+
--resume <thread> Continue an earlier thread instead of starting one
|
|
27
|
+
--prompt-file <f> Read the prompt from a file
|
|
28
|
+
--schema <file> JSON Schema the final message must match
|
|
29
|
+
--timeout <sec> Give up after this many seconds (default ${DEFAULT_TIMEOUT_SECONDS})
|
|
30
|
+
--trace Also print the command and file-change trace
|
|
31
|
+
-h, --help Print this help`;
|
|
32
|
+
|
|
33
|
+
const parseArgs = (argv) => {
|
|
34
|
+
const options = {
|
|
35
|
+
model: DEFAULT_MODEL,
|
|
36
|
+
effort: '',
|
|
37
|
+
write: false,
|
|
38
|
+
cd: process.cwd(),
|
|
39
|
+
resume: '',
|
|
40
|
+
promptFile: '',
|
|
41
|
+
schema: '',
|
|
42
|
+
timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
43
|
+
trace: false,
|
|
44
|
+
prompt: '',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const rest = [];
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
50
|
+
const arg = argv[index];
|
|
51
|
+
const next = () => {
|
|
52
|
+
index += 1;
|
|
53
|
+
if (index >= argv.length) throw new Error(`${arg} needs a value.`);
|
|
54
|
+
return argv[index];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (arg === '-m' || arg === '--model') options.model = next();
|
|
58
|
+
else if (arg === '-e' || arg === '--effort') options.effort = next();
|
|
59
|
+
else if (arg === '-w' || arg === '--write') options.write = true;
|
|
60
|
+
else if (arg === '-C' || arg === '--cd') options.cd = next();
|
|
61
|
+
else if (arg === '--resume') options.resume = next();
|
|
62
|
+
else if (arg === '--prompt-file') options.promptFile = next();
|
|
63
|
+
else if (arg === '--schema') options.schema = next();
|
|
64
|
+
else if (arg === '--timeout') options.timeout = Number(next());
|
|
65
|
+
else if (arg === '--trace') options.trace = true;
|
|
66
|
+
else if (arg === '-h' || arg === '--help') options.help = true;
|
|
67
|
+
else if (arg.startsWith('-') && arg !== '-') throw new Error(`Unknown option ${arg}.`);
|
|
68
|
+
else rest.push(arg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
options.prompt = options.promptFile ? readFileSync(options.promptFile, 'utf8') : rest.join(' ');
|
|
72
|
+
|
|
73
|
+
return options;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const readStdin = () =>
|
|
77
|
+
new Promise((resolve) => {
|
|
78
|
+
let buffer = '';
|
|
79
|
+
process.stdin.setEncoding('utf8');
|
|
80
|
+
process.stdin.on('data', (chunk) => (buffer += chunk));
|
|
81
|
+
process.stdin.on('end', () => resolve(buffer));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const buildArgs = (options, lastMessageFile) => {
|
|
85
|
+
const args = ['exec'];
|
|
86
|
+
|
|
87
|
+
if (options.resume) args.push('resume');
|
|
88
|
+
|
|
89
|
+
args.push('--json', '--skip-git-repo-check', '-o', lastMessageFile, '-m', options.model);
|
|
90
|
+
|
|
91
|
+
// `codex exec resume` carries the sandbox and the working root of the session it resumes,
|
|
92
|
+
// and rejects both flags, so only a fresh run may pass them.
|
|
93
|
+
if (!options.resume) {
|
|
94
|
+
args.push('-C', options.cd, '--sandbox', options.write ? 'workspace-write' : 'read-only');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (options.effort) args.push('-c', `model_reasoning_effort="${options.effort}"`);
|
|
98
|
+
if (options.schema) args.push('--output-schema', options.schema);
|
|
99
|
+
if (options.resume) args.push(options.resume);
|
|
100
|
+
|
|
101
|
+
args.push('-');
|
|
102
|
+
|
|
103
|
+
return args;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const describeItem = (item) => {
|
|
107
|
+
if (item.type === 'command_execution') return `$ ${String(item.command ?? '').split('\n')[0]}`;
|
|
108
|
+
if (item.type === 'file_change') {
|
|
109
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
110
|
+
return `~ ${changes.map((change) => `${change.kind ?? 'edit'} ${change.path ?? ''}`).join(', ')}`;
|
|
111
|
+
}
|
|
112
|
+
if (item.type === 'error') return `! ${item.message ?? 'unknown error'}`;
|
|
113
|
+
return '';
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const duration = (milliseconds) => {
|
|
117
|
+
const seconds = Math.round(milliseconds / 1000);
|
|
118
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const run = async () => {
|
|
122
|
+
const options = parseArgs(process.argv.slice(2));
|
|
123
|
+
|
|
124
|
+
if (options.help) {
|
|
125
|
+
process.stdout.write(`${USAGE}\n`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!options.prompt.trim() && !process.stdin.isTTY) options.prompt = await readStdin();
|
|
130
|
+
|
|
131
|
+
if (!options.prompt.trim()) {
|
|
132
|
+
process.stderr.write(`${USAGE}\n`);
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const logDir = join(tmpdir(), 'codex-agent');
|
|
137
|
+
mkdirSync(logDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
140
|
+
const logFile = join(logDir, `${stamp}.jsonl`);
|
|
141
|
+
const lastMessageFile = join(logDir, `${stamp}.last.md`);
|
|
142
|
+
const log = createWriteStream(logFile);
|
|
143
|
+
const started = Date.now();
|
|
144
|
+
|
|
145
|
+
const child = spawn('codex', buildArgs(options, lastMessageFile), {
|
|
146
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
147
|
+
env: process.env,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
child.stdin.end(options.prompt);
|
|
151
|
+
|
|
152
|
+
const state = { thread: '', usage: null, errors: [], trace: [], failed: false };
|
|
153
|
+
|
|
154
|
+
createInterface({ input: child.stdout }).on('line', (line) => {
|
|
155
|
+
log.write(`${line}\n`);
|
|
156
|
+
|
|
157
|
+
let event;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
event = JSON.parse(line);
|
|
161
|
+
} catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (event.type === 'thread.started') state.thread = event.thread_id ?? '';
|
|
166
|
+
else if (event.type === 'turn.completed') state.usage = event.usage ?? null;
|
|
167
|
+
else if (event.type === 'turn.failed') {
|
|
168
|
+
state.failed = true;
|
|
169
|
+
state.errors.push(event.error?.message ?? 'the turn failed');
|
|
170
|
+
} else if (event.type === 'item.completed') {
|
|
171
|
+
const description = describeItem(event.item ?? {});
|
|
172
|
+
if (description) state.trace.push(description);
|
|
173
|
+
if (event.item?.type === 'error') state.errors.push(event.item.message ?? 'unknown error');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let stderrText = '';
|
|
178
|
+
child.stderr.setEncoding('utf8');
|
|
179
|
+
child.stderr.on('data', (chunk) => (stderrText += chunk));
|
|
180
|
+
|
|
181
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), Math.max(1, options.timeout) * 1000);
|
|
182
|
+
const code = await new Promise((resolve) => child.on('close', resolve));
|
|
183
|
+
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
log.end();
|
|
186
|
+
|
|
187
|
+
let finalMessage;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
finalMessage = readFileSync(lastMessageFile, 'utf8').trim();
|
|
191
|
+
rmSync(lastMessageFile, { force: true });
|
|
192
|
+
} catch {
|
|
193
|
+
finalMessage = '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const failed = code !== 0 || state.failed || (!finalMessage && state.errors.length > 0);
|
|
197
|
+
|
|
198
|
+
if (options.trace && state.trace.length > 0) process.stdout.write(`${state.trace.join('\n')}\n\n`);
|
|
199
|
+
if (finalMessage) process.stdout.write(`${finalMessage}\n`);
|
|
200
|
+
|
|
201
|
+
if (failed) {
|
|
202
|
+
const reason = state.errors.join('\n') || stderrText.trim() || `codex exited with code ${code}`;
|
|
203
|
+
process.stdout.write(`\ncodex agent FAILED: ${reason}\n`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const parts = [
|
|
207
|
+
`codex ${options.model}`,
|
|
208
|
+
options.write ? 'workspace-write' : 'read-only',
|
|
209
|
+
duration(Date.now() - started),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
if (state.usage) parts.push(`${state.usage.output_tokens ?? 0} out tok`);
|
|
213
|
+
|
|
214
|
+
process.stdout.write(`\n--- ${parts.join(' · ')}\n`);
|
|
215
|
+
|
|
216
|
+
if (state.thread) process.stdout.write(`resume: --resume ${state.thread}\n`);
|
|
217
|
+
|
|
218
|
+
process.stdout.write(`log: ${logFile}\n`);
|
|
219
|
+
|
|
220
|
+
return failed ? 1 : 0;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
run().then(
|
|
224
|
+
(code) => process.exit(code),
|
|
225
|
+
(error) => {
|
|
226
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
227
|
+
process.exit(2);
|
|
228
|
+
},
|
|
229
|
+
);
|