job-forge 2.14.9 → 2.14.10
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/.cursor/rules/main.mdc +4 -1
- package/.opencode/skills/job-forge.md +3 -0
- package/AGENTS.md +4 -1
- package/CLAUDE.md +4 -1
- package/bin/create-job-forge.mjs +3 -0
- package/bin/job-forge.mjs +29 -0
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/CUSTOMIZATION.md +13 -3
- package/docs/SETUP.md +3 -0
- package/iso/commands/job-forge.md +3 -0
- package/iso/instructions.md +4 -1
- package/package.json +5 -5
- package/scripts/trace.mjs +469 -0
package/.cursor/rules/main.mdc
CHANGED
|
@@ -33,6 +33,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
33
33
|
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`.
|
|
34
34
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
35
35
|
|
|
36
|
+
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
37
|
+
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions
|
|
38
|
+
|
|
36
39
|
## Defaults
|
|
37
40
|
|
|
38
41
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -58,7 +61,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
58
61
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
59
62
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
60
63
|
3. Read the active mode file [D3]; decide inline vs delegated work [D1].
|
|
61
|
-
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
|
|
64
|
+
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
|
|
62
65
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
63
66
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
64
67
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
@@ -152,6 +152,9 @@ Step 3 — Pre-flight cleanup (once, before the loop)
|
|
|
152
152
|
Step 4 — Loop in rounds of 2 (Hard Limit #1)
|
|
153
153
|
for round in ceil(len(candidates) / 2):
|
|
154
154
|
pair = candidates[round*2 : round*2 + 2]
|
|
155
|
+
# If proxy is configured, do not paste proxy values into prompts.
|
|
156
|
+
# Say: "Proxy is configured; read config/profile.yml and pass its
|
|
157
|
+
# top-level proxy object to every geometra_connect call."
|
|
155
158
|
# Dispatch 1 or 2 task() calls in ONE message (never 3+)
|
|
156
159
|
task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
|
|
157
160
|
task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
|
package/AGENTS.md
CHANGED
|
@@ -28,6 +28,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
28
28
|
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
|
+
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
32
|
+
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions
|
|
33
|
+
|
|
31
34
|
## Defaults
|
|
32
35
|
|
|
33
36
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -53,7 +56,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
53
56
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
54
57
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
55
58
|
3. Read the active mode file [D3]; decide inline vs delegated work [D1].
|
|
56
|
-
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
|
|
59
|
+
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
|
|
57
60
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
58
61
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
59
62
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/CLAUDE.md
CHANGED
|
@@ -28,6 +28,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
28
28
|
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
|
+
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
32
|
+
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions
|
|
33
|
+
|
|
31
34
|
## Defaults
|
|
32
35
|
|
|
33
36
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -53,7 +56,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
53
56
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
54
57
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
55
58
|
3. Read the active mode file [D3]; decide inline vs delegated work [D1].
|
|
56
|
-
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
|
|
59
|
+
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
|
|
57
60
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
58
61
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
59
62
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/bin/create-job-forge.mjs
CHANGED
|
@@ -110,6 +110,9 @@ const consumerPkg = {
|
|
|
110
110
|
tokens: 'job-forge tokens',
|
|
111
111
|
'tokens:today': 'job-forge tokens --days 1',
|
|
112
112
|
'tokens:log': 'job-forge tokens --days 1 --append',
|
|
113
|
+
'trace:list': 'job-forge trace:list',
|
|
114
|
+
'trace:stats': 'job-forge trace:stats',
|
|
115
|
+
'trace:show': 'job-forge trace:show',
|
|
113
116
|
// One command to pull the latest harness, companion plugin, and any
|
|
114
117
|
// locally-pinned MCP packages. npm update is a no-op on packages not
|
|
115
118
|
// in package.json, so listing @razroo/gmail-mcp + @geometra/mcp is
|
package/bin/job-forge.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* pdf Run generate-pdf.mjs
|
|
18
18
|
* sync-check Run cv-sync-check.mjs
|
|
19
19
|
* tokens Run scripts/token-usage-report.mjs
|
|
20
|
+
* trace:* Inspect local agent transcripts via iso-trace
|
|
20
21
|
* sync Re-run the harness symlink sync (bin/sync.mjs)
|
|
21
22
|
* help, --help Show this message
|
|
22
23
|
*/
|
|
@@ -28,6 +29,7 @@ import { existsSync } from 'fs';
|
|
|
28
29
|
|
|
29
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
32
|
+
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
31
33
|
|
|
32
34
|
const commands = {
|
|
33
35
|
merge: 'merge-tracker.mjs',
|
|
@@ -52,6 +54,12 @@ const commands = {
|
|
|
52
54
|
'render-report-header': 'scripts/render-report-header.mjs',
|
|
53
55
|
};
|
|
54
56
|
|
|
57
|
+
const traceAliases = {
|
|
58
|
+
'trace:list': 'list',
|
|
59
|
+
'trace:stats': 'stats',
|
|
60
|
+
'trace:show': 'show',
|
|
61
|
+
};
|
|
62
|
+
|
|
55
63
|
const [, , cmd, ...rest] = process.argv;
|
|
56
64
|
|
|
57
65
|
function printHelp() {
|
|
@@ -68,6 +76,10 @@ Commands:
|
|
|
68
76
|
pdf Generate ATS-optimized CV PDF from cv.md
|
|
69
77
|
sync-check Lint: verify cv.md and profile.yml are filled in
|
|
70
78
|
tokens Show opencode token usage and cost by session/day
|
|
79
|
+
trace Pass through to iso-trace (e.g. job-forge trace sources)
|
|
80
|
+
trace:list List recent local agent sessions (defaults: --since 7d --cwd project)
|
|
81
|
+
trace:stats Show trace stats (defaults: --since 7d --cwd project)
|
|
82
|
+
trace:show ID Show one trace by id or prefix
|
|
71
83
|
sync Re-create harness symlinks in the current project
|
|
72
84
|
|
|
73
85
|
Deterministic helpers (prefer these over LLM-derived values):
|
|
@@ -90,6 +102,8 @@ Pass --help after a command to see its own flags, e.g.:
|
|
|
90
102
|
job-forge merge --help
|
|
91
103
|
job-forge tokens --days 1
|
|
92
104
|
job-forge slugify "Anthropic, PBC"
|
|
105
|
+
job-forge trace:list --since 24h
|
|
106
|
+
job-forge trace:show ses_...
|
|
93
107
|
|
|
94
108
|
Project directory resolves to $JOB_FORGE_PROJECT or cwd.`);
|
|
95
109
|
}
|
|
@@ -99,6 +113,21 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
99
113
|
process.exit(0);
|
|
100
114
|
}
|
|
101
115
|
|
|
116
|
+
if (cmd === 'trace' || traceAliases[cmd]) {
|
|
117
|
+
const traceArgs = cmd === 'trace'
|
|
118
|
+
? (rest.length === 0 ? ['help'] : rest)
|
|
119
|
+
: [traceAliases[cmd], ...rest];
|
|
120
|
+
|
|
121
|
+
const scriptPath = join(PKG_ROOT, 'scripts/trace.mjs');
|
|
122
|
+
const result = spawnSync(process.execPath, [scriptPath, ...traceArgs], {
|
|
123
|
+
stdio: 'inherit',
|
|
124
|
+
cwd: PROJECT_DIR,
|
|
125
|
+
env: process.env,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
process.exit(result.status ?? 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
const rel = commands[cmd];
|
|
103
132
|
if (!rel) {
|
|
104
133
|
console.error(`Unknown command: ${cmd}\n`);
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -203,6 +203,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
|
|
|
203
203
|
| `generate-pdf.mjs` | `npx job-forge pdf` | Renders HTML to PDF via Geometra MCP (`geometra_generate_pdf`) or standalone Playwright/Chromium (`npx job-forge pdf <input.html> <output.pdf>`) |
|
|
204
204
|
| `cv-sync-check.mjs` | `npx job-forge sync-check` | Setup lint: `cv.md` + `config/profile.yml`, hardcoded-metric scan on `modes/_shared.md` and `batch/batch-prompt.md`, optional `article-digest.md` freshness |
|
|
205
205
|
| `scripts/token-usage-report.mjs` | `npx job-forge tokens` | Per-session opencode token/cost report from the SQLite DB |
|
|
206
|
+
| `scripts/trace.mjs` | `npx job-forge trace:list` / `trace:stats` / `trace:show` | Local transcript observability via `@razroo/iso-trace`; common commands default to OpenCode sessions for the consumer project |
|
|
206
207
|
| `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
|
|
207
208
|
| `bin/sync.mjs` | `npx job-forge sync` | Creates the harness symlinks in a consumer project (also runs as `postinstall`) |
|
|
208
209
|
| `bin/create-job-forge.mjs` | `npx create-job-forge <dir>` | Scaffolds a new personal project |
|
package/docs/CUSTOMIZATION.md
CHANGED
|
@@ -98,13 +98,23 @@ Some forks use a **single** `data/applications.md` instead. That is fine if you
|
|
|
98
98
|
|
|
99
99
|
## Transcript observability (iso-trace)
|
|
100
100
|
|
|
101
|
-
To inspect real agent sessions locally (tool mix, redundant fetches, Geometra churn) without uploading transcripts, use Razroo's [`@razroo/iso-trace`](https://github.com/razroo/iso/tree/main/packages/iso-trace).
|
|
101
|
+
To inspect real agent sessions locally (tool mix, redundant fetches, Geometra churn) without uploading transcripts, use the `job-forge trace:*` commands. JobForge depends on Razroo's [`@razroo/iso-trace`](https://github.com/razroo/iso/tree/main/packages/iso-trace), so consumer projects do not need to install it separately.
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Common commands default to OpenCode sessions for the current project and use a 7-day window:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx job-forge trace:list
|
|
107
|
+
npx job-forge trace:stats
|
|
108
|
+
npx job-forge trace:show <session-id-or-prefix>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Scaffolded projects also include npm aliases: `npm run trace:list`, `npm run trace:stats`, and `npm run trace:show -- <id>`.
|
|
112
|
+
|
|
113
|
+
For raw iso-trace commands, use `npx job-forge trace sources`, `npx job-forge trace where`, or any other `iso-trace` subcommand after `trace`.
|
|
104
114
|
|
|
105
115
|
**Where Claude Code writes JSONL:** `~/.claude/projects/<encoded-cwd>/*.jsonl`.
|
|
106
116
|
|
|
107
|
-
**CLI
|
|
117
|
+
**Direct CLI fallback:** `npx -y @razroo/iso-trace@latest stats --source "$HOME/.claude/projects/<encoded-dir>/<session>.jsonl"`
|
|
108
118
|
|
|
109
119
|
**Performance:** `iso-trace list --cwd /path/to/repo` walks all of `~/.claude/projects` before filtering; on large machines prefer `stats --source <one.jsonl>` or the library's `discoverSessions({ roots: ["<one encoded project dir>"] })` (see the iso-trace README).
|
|
110
120
|
|
package/docs/SETUP.md
CHANGED
|
@@ -130,6 +130,9 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
130
130
|
| Generate ATS-optimized CV PDF | `npx job-forge pdf` | `npm run pdf` |
|
|
131
131
|
| Setup lint (cv.md + profile.yml) | `npx job-forge sync-check` | `npm run sync-check` |
|
|
132
132
|
| Token usage report (from opencode SQLite DB) | `npx job-forge tokens` | `npm run tokens` |
|
|
133
|
+
| List recent OpenCode traces for this project | `npx job-forge trace:list` | `npm run trace:list` |
|
|
134
|
+
| Summarize trace tool/file/token usage | `npx job-forge trace:stats` | `npm run trace:stats` |
|
|
135
|
+
| Show one trace by session id/prefix | `npx job-forge trace:show <id>` | `npm run trace:show -- <id>` |
|
|
133
136
|
| Re-create harness symlinks | `npx job-forge sync` | `npm run sync` |
|
|
134
137
|
| Build optional dashboard TUI (Go on `PATH`) | `(cd node_modules/job-forge/dashboard && go build .)` | `npm run build:dashboard` (harness repo only) |
|
|
135
138
|
|
|
@@ -155,6 +155,9 @@ Step 3 — Pre-flight cleanup (once, before the loop)
|
|
|
155
155
|
Step 4 — Loop in rounds of 2 (Hard Limit #1)
|
|
156
156
|
for round in ceil(len(candidates) / 2):
|
|
157
157
|
pair = candidates[round*2 : round*2 + 2]
|
|
158
|
+
# If proxy is configured, do not paste proxy values into prompts.
|
|
159
|
+
# Say: "Proxy is configured; read config/profile.yml and pass its
|
|
160
|
+
# top-level proxy object to every geometra_connect call."
|
|
158
161
|
# Dispatch 1 or 2 task() calls in ONE message (never 3+)
|
|
159
162
|
task(subagent_type=<tier per AGENTS.md routing>, prompt=<apply prompt for pair[0]>)
|
|
160
163
|
task(subagent_type=<tier>, prompt=<apply prompt for pair[1]>) # only if pair has 2
|
package/iso/instructions.md
CHANGED
|
@@ -28,6 +28,9 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
28
28
|
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, `batch/tracker-additions/*.tsv`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
|
+
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object to every `geometra_connect` call." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
32
|
+
why: a 2026-04-25 OpenCode trace showed raw proxy credentials copied into an apply subagent prompt; trace logs are local, but prompts must still avoid replicating secrets across subagent sessions
|
|
33
|
+
|
|
31
34
|
## Defaults
|
|
32
35
|
|
|
33
36
|
- [D1] Delegate to a subagent (`task`) only when the work involves repeated tool-heavy steps that bloat the cache prefix: applying to N≥2 jobs, batch scans hitting ≥3 companies, or any "apply to… / process pipeline / batch evaluate" user phrasing. Single-offer evals, dev work, file edits, `tracker` mode, single-URL checks, and one-shot questions stay inline.
|
|
@@ -53,7 +56,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
53
56
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
54
57
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
55
58
|
3. Read the active mode file [D3]; decide inline vs delegated work [D1].
|
|
56
|
-
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2].
|
|
59
|
+
4. Prepare Geometra dispatches: cleanup [H3], dedupe [H2], location filter [D5], routing [D2], proxy prompt hygiene [H8].
|
|
57
60
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b].
|
|
58
61
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
59
62
|
7. Cross-check subagent facts against authoritative files [H7].
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.10",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"tokens": "node scripts/token-usage-report.mjs",
|
|
19
19
|
"tokens:today": "node scripts/token-usage-report.mjs --days 1",
|
|
20
20
|
"tokens:log": "node scripts/token-usage-report.mjs --days 1 --append",
|
|
21
|
-
"trace:list": "
|
|
22
|
-
"trace:stats": "
|
|
23
|
-
"trace:show": "
|
|
21
|
+
"trace:list": "node bin/job-forge.mjs trace:list",
|
|
22
|
+
"trace:stats": "node bin/job-forge.mjs trace:stats",
|
|
23
|
+
"trace:show": "node bin/job-forge.mjs trace:show",
|
|
24
24
|
"plan": "iso plan .",
|
|
25
25
|
"lint:agentmd": "agentmd lint iso/instructions.md",
|
|
26
26
|
"lint:modes": "isolint lint modes/",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"node": ">=18"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
+
"@razroo/iso-trace": "^0.4.0",
|
|
88
89
|
"playwright": "^1.58.1"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
@@ -93,7 +94,6 @@
|
|
|
93
94
|
"@razroo/iso-eval": "^0.4.0",
|
|
94
95
|
"@razroo/iso-harness": "^0.6.1",
|
|
95
96
|
"@razroo/iso-route": "^0.5.3",
|
|
96
|
-
"@razroo/iso-trace": "^0.4.0",
|
|
97
97
|
"@razroo/opencode-model-fallback": "^0.3.1"
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { dirname, join, resolve } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import {
|
|
9
|
+
defaultOpenCodeDbPath,
|
|
10
|
+
findSessionById,
|
|
11
|
+
openCodeSessionLocator,
|
|
12
|
+
parseSinceCutoff,
|
|
13
|
+
} from '@razroo/iso-trace';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
18
|
+
|
|
19
|
+
const USAGE = `job-forge trace — local OpenCode transcript observability
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
job-forge trace:list [--since 7d] [--cwd <dir>] [--json]
|
|
23
|
+
job-forge trace:stats [<id-or-prefix>...] [--since 7d] [--cwd <dir>] [--json]
|
|
24
|
+
job-forge trace:show <id-or-prefix> [--events <kinds>] [--grep <regex>]
|
|
25
|
+
job-forge trace <iso-trace args...>
|
|
26
|
+
|
|
27
|
+
Common aliases default to OpenCode sessions for the current JobForge project.
|
|
28
|
+
Use "job-forge trace sources" or "job-forge trace where" for raw iso-trace passthrough.`;
|
|
29
|
+
|
|
30
|
+
const [cmd = 'help', ...args] = process.argv.slice(2);
|
|
31
|
+
|
|
32
|
+
function parseFilters(rawArgs) {
|
|
33
|
+
const opts = { since: '7d', cwd: PROJECT_DIR, json: false };
|
|
34
|
+
const positional = [];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
37
|
+
const arg = rawArgs[i];
|
|
38
|
+
if (arg === '--since') {
|
|
39
|
+
opts.since = rawArgs[++i];
|
|
40
|
+
} else if (arg.startsWith('--since=')) {
|
|
41
|
+
opts.since = arg.slice('--since='.length);
|
|
42
|
+
} else if (arg === '--cwd') {
|
|
43
|
+
opts.cwd = rawArgs[++i];
|
|
44
|
+
} else if (arg.startsWith('--cwd=')) {
|
|
45
|
+
opts.cwd = arg.slice('--cwd='.length);
|
|
46
|
+
} else if (arg === '--json') {
|
|
47
|
+
opts.json = true;
|
|
48
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
49
|
+
opts.help = true;
|
|
50
|
+
} else if (arg.startsWith('--')) {
|
|
51
|
+
opts.error = `unknown flag "${arg}"`;
|
|
52
|
+
} else {
|
|
53
|
+
positional.push(arg);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
opts.cwd = resolve(opts.cwd || PROJECT_DIR);
|
|
58
|
+
return { opts, positional };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseShowArgs(rawArgs) {
|
|
62
|
+
const opts = {};
|
|
63
|
+
const positional = [];
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
66
|
+
const arg = rawArgs[i];
|
|
67
|
+
if (arg === '--events') {
|
|
68
|
+
const raw = rawArgs[++i] || '';
|
|
69
|
+
opts.events = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
|
|
70
|
+
} else if (arg.startsWith('--events=')) {
|
|
71
|
+
const raw = arg.slice('--events='.length);
|
|
72
|
+
opts.events = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
|
|
73
|
+
} else if (arg === '--grep') {
|
|
74
|
+
opts.grep = compileRegex(rawArgs[++i], 'trace:show');
|
|
75
|
+
} else if (arg.startsWith('--grep=')) {
|
|
76
|
+
opts.grep = compileRegex(arg.slice('--grep='.length), 'trace:show');
|
|
77
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
78
|
+
opts.help = true;
|
|
79
|
+
} else if (arg.startsWith('--')) {
|
|
80
|
+
opts.error = `unknown flag "${arg}"`;
|
|
81
|
+
} else {
|
|
82
|
+
positional.push(arg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { opts, positional };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function compileRegex(pattern, context) {
|
|
90
|
+
try {
|
|
91
|
+
return new RegExp(pattern || '', 'i');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
return new Error(`${context}: invalid --grep regex: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function discoverOpenCodeRefs(opts) {
|
|
99
|
+
const dbPath = defaultOpenCodeDbPath();
|
|
100
|
+
if (!existsSync(dbPath)) return [];
|
|
101
|
+
|
|
102
|
+
const where = [
|
|
103
|
+
's.time_archived is null',
|
|
104
|
+
`s.directory = ${sqlString(resolve(opts.cwd || PROJECT_DIR))}`,
|
|
105
|
+
];
|
|
106
|
+
const sinceMs = parseSinceCutoff(opts.since);
|
|
107
|
+
if (sinceMs !== undefined) {
|
|
108
|
+
where.push(`s.time_created >= ${Number(sinceMs)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rows = queryOpenCodeDb(dbPath, [
|
|
112
|
+
'select',
|
|
113
|
+
' s.id,',
|
|
114
|
+
' s.directory,',
|
|
115
|
+
' s.time_created,',
|
|
116
|
+
' s.time_updated,',
|
|
117
|
+
' (select count(*) from message m where m.session_id = s.id) as turn_count,',
|
|
118
|
+
' (',
|
|
119
|
+
' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
|
|
120
|
+
' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
|
|
121
|
+
' ) as size_bytes',
|
|
122
|
+
'from session s',
|
|
123
|
+
`where ${where.join(' and ')}`,
|
|
124
|
+
'order by s.time_updated desc',
|
|
125
|
+
].join(' '));
|
|
126
|
+
|
|
127
|
+
return rows.map((row) => ({
|
|
128
|
+
id: row.id,
|
|
129
|
+
source: {
|
|
130
|
+
harness: 'opencode',
|
|
131
|
+
format: 'opencode/sqlite-v1',
|
|
132
|
+
path: openCodeSessionLocator(row.id, dbPath),
|
|
133
|
+
},
|
|
134
|
+
cwd: row.directory,
|
|
135
|
+
startedAt: msToIso(row.time_created),
|
|
136
|
+
endedAt: msToIso(row.time_updated),
|
|
137
|
+
turnCount: row.turn_count ?? 0,
|
|
138
|
+
sizeBytes: row.size_bytes ?? 0,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function queryOpenCodeDb(dbPath, sql) {
|
|
143
|
+
const result = spawnSync('sqlite3', ['-json', dbPath, sql], {
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
146
|
+
});
|
|
147
|
+
if ((result.status ?? 0) !== 0) {
|
|
148
|
+
const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
|
|
149
|
+
throw new Error(`job-forge trace: sqlite3 query failed: ${detail}`);
|
|
150
|
+
}
|
|
151
|
+
return JSON.parse(result.stdout || '[]');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sqlString(value) {
|
|
155
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function msToIso(ms) {
|
|
159
|
+
return new Date(Number(ms)).toISOString();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sizeLabel(bytes) {
|
|
163
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
164
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
165
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shorten(value, max) {
|
|
169
|
+
const text = String(value ?? '');
|
|
170
|
+
if (text.length <= max) return text;
|
|
171
|
+
return `${text.slice(0, max - 1)}...`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function pad(value, width) {
|
|
175
|
+
const text = String(value ?? '');
|
|
176
|
+
return text.length >= width ? text : text + ' '.repeat(width - text.length);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function printSessionTable(refs) {
|
|
180
|
+
const rows = refs.map((ref) => [
|
|
181
|
+
ref.id,
|
|
182
|
+
ref.startedAt.replace('T', ' ').replace(/\.\d+Z$/, 'Z'),
|
|
183
|
+
shorten(ref.cwd, 42),
|
|
184
|
+
String(ref.turnCount),
|
|
185
|
+
sizeLabel(ref.sizeBytes),
|
|
186
|
+
]);
|
|
187
|
+
const header = ['id', 'started', 'cwd', 'turns', 'size'];
|
|
188
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
189
|
+
|
|
190
|
+
console.log(header.map((h, i) => pad(h, widths[i])).join(' '));
|
|
191
|
+
console.log(widths.map((w) => '-'.repeat(w)).join(' '));
|
|
192
|
+
for (const row of rows) {
|
|
193
|
+
console.log(row.map((cell, i) => pad(cell, widths[i])).join(' '));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printStats(result) {
|
|
198
|
+
console.log(`sessions: ${result.sessions}`);
|
|
199
|
+
console.log(`turns: ${result.turns}`);
|
|
200
|
+
console.log(`duration: ${Math.round(result.durationMs / 1000)}s`);
|
|
201
|
+
console.log(`tokens: input=${result.tokens.input} output=${result.tokens.output} cache_read=${result.tokens.cacheRead} cache_created=${result.tokens.cacheCreated}`);
|
|
202
|
+
|
|
203
|
+
console.log('\ntool calls:');
|
|
204
|
+
for (const [name, count] of Object.entries(result.toolCalls).sort((a, b) => b[1] - a[1])) {
|
|
205
|
+
console.log(` ${pad(name, 28)} ${count}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('\nfile ops:');
|
|
209
|
+
for (const [name, count] of Object.entries(result.fileOps).sort((a, b) => b[1] - a[1])) {
|
|
210
|
+
console.log(` ${pad(name, 8)} ${count}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function computeOpenCodeStats(refs) {
|
|
215
|
+
const result = {
|
|
216
|
+
sessions: refs.length,
|
|
217
|
+
turns: 0,
|
|
218
|
+
durationMs: 0,
|
|
219
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheCreated: 0 },
|
|
220
|
+
toolCalls: {},
|
|
221
|
+
fileOps: {},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const ref of refs) {
|
|
225
|
+
const rows = loadOpenCodeRows(ref.id);
|
|
226
|
+
result.turns += rows.messages.length;
|
|
227
|
+
result.durationMs += Math.max(0, Date.parse(ref.endedAt || ref.startedAt) - Date.parse(ref.startedAt));
|
|
228
|
+
|
|
229
|
+
for (const row of rows.messages) {
|
|
230
|
+
const data = parseJson(row.data);
|
|
231
|
+
const tokens = data.tokens;
|
|
232
|
+
if (!tokens || typeof tokens !== 'object') continue;
|
|
233
|
+
result.tokens.input += Number(tokens.input || 0);
|
|
234
|
+
result.tokens.output += Number(tokens.output || 0);
|
|
235
|
+
result.tokens.cacheRead += Number(tokens.cache?.read || 0);
|
|
236
|
+
result.tokens.cacheCreated += Number(tokens.cache?.write || 0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const row of rows.parts) {
|
|
240
|
+
const data = parseJson(row.data);
|
|
241
|
+
if (data.type !== 'tool') continue;
|
|
242
|
+
const toolName = data.tool || 'unknown';
|
|
243
|
+
result.toolCalls[toolName] = (result.toolCalls[toolName] || 0) + 1;
|
|
244
|
+
const op = fileOpForTool(toolName);
|
|
245
|
+
if (op) result.fileOps[op] = (result.fileOps[op] || 0) + 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function printOpenCodeSession(ref, opts) {
|
|
253
|
+
const rows = loadOpenCodeRows(ref.id);
|
|
254
|
+
console.log(`id: ${ref.id}`);
|
|
255
|
+
console.log(`source: ${ref.source.harness} (${ref.source.format})`);
|
|
256
|
+
console.log(`path: ${ref.source.path}`);
|
|
257
|
+
console.log(`cwd: ${ref.cwd}`);
|
|
258
|
+
console.log(`started: ${ref.startedAt}`);
|
|
259
|
+
if (ref.endedAt) console.log(`ended: ${ref.endedAt}`);
|
|
260
|
+
console.log(`turns: ${rows.messages.length}`);
|
|
261
|
+
console.log('');
|
|
262
|
+
|
|
263
|
+
const events = openCodeEvents(rows);
|
|
264
|
+
for (const event of events) {
|
|
265
|
+
if (opts.events && !opts.events.has(event.kind)) continue;
|
|
266
|
+
const line = formatOpenCodeEvent(event);
|
|
267
|
+
if (opts.grep && !opts.grep.test(line)) continue;
|
|
268
|
+
console.log(line);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function loadOpenCodeRows(sessionId) {
|
|
273
|
+
const dbPath = defaultOpenCodeDbPath();
|
|
274
|
+
const id = sqlString(sessionId);
|
|
275
|
+
return {
|
|
276
|
+
messages: queryOpenCodeDb(dbPath, `select id, time_created, data from message where session_id = ${id} order by time_created, id`),
|
|
277
|
+
parts: queryOpenCodeDb(dbPath, `select id, message_id, time_created, data from part where session_id = ${id} order by time_created, id`),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function openCodeEvents(rows) {
|
|
282
|
+
const events = [];
|
|
283
|
+
|
|
284
|
+
for (const row of rows.messages) {
|
|
285
|
+
const data = parseJson(row.data);
|
|
286
|
+
const at = msToIso(row.time_created);
|
|
287
|
+
const model = data.modelID && data.providerID ? `${data.providerID}/${data.modelID}` : undefined;
|
|
288
|
+
const error = data.error?.data?.message || data.error?.message;
|
|
289
|
+
events.push({
|
|
290
|
+
kind: error ? 'error' : 'turn',
|
|
291
|
+
at,
|
|
292
|
+
text: error
|
|
293
|
+
? `${data.role || 'assistant'} ${data.agent || ''} ${model || ''}: ${error}`
|
|
294
|
+
: `${data.role || 'unknown'} ${data.agent || ''} ${model || ''} finish=${data.finish || 'unknown'}`,
|
|
295
|
+
});
|
|
296
|
+
if (data.tokens) {
|
|
297
|
+
events.push({
|
|
298
|
+
kind: 'token_usage',
|
|
299
|
+
at,
|
|
300
|
+
text: `input=${data.tokens.input || 0} output=${data.tokens.output || 0} cache_read=${data.tokens.cache?.read || 0} cache_created=${data.tokens.cache?.write || 0}${model ? ` model=${model}` : ''}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const row of rows.parts) {
|
|
306
|
+
const data = parseJson(row.data);
|
|
307
|
+
const at = msToIso(row.time_created);
|
|
308
|
+
if (data.type === 'text') {
|
|
309
|
+
events.push({ kind: 'message', at, text: data.text || '' });
|
|
310
|
+
} else if (data.type === 'reasoning') {
|
|
311
|
+
events.push({ kind: 'reasoning', at, text: data.text || '' });
|
|
312
|
+
} else if (data.type === 'tool') {
|
|
313
|
+
const status = data.state?.status ? ` status=${data.state.status}` : '';
|
|
314
|
+
const input = data.state?.input ? ` ${JSON.stringify(data.state.input)}` : '';
|
|
315
|
+
const output = data.state?.output ? ` => ${data.state.output}` : '';
|
|
316
|
+
events.push({ kind: 'tool_call', at, text: `${data.tool || 'unknown'}${status}${input}${output}` });
|
|
317
|
+
const op = fileOpForTool(data.tool);
|
|
318
|
+
if (op) events.push({ kind: 'file_op', at, text: `${op} ${filePathFromTool(data) || ''}`.trim() });
|
|
319
|
+
} else if (data.__parseError) {
|
|
320
|
+
events.push({ kind: 'error', at, text: `unparseable part JSON: ${data.__parseError}` });
|
|
321
|
+
} else {
|
|
322
|
+
events.push({ kind: data.type || 'part', at, text: JSON.stringify(data) });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return events.sort((a, b) => a.at.localeCompare(b.at));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatOpenCodeEvent(event) {
|
|
330
|
+
return `${event.at} ${event.kind}: ${oneLine(event.text, 360)}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseJson(raw) {
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(raw || '{}');
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
338
|
+
return { __parseError: message, __raw: raw };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function fileOpForTool(toolName) {
|
|
343
|
+
if (toolName === 'read') return 'read';
|
|
344
|
+
if (toolName === 'write') return 'write';
|
|
345
|
+
if (toolName === 'edit') return 'edit';
|
|
346
|
+
if (toolName === 'glob') return 'list';
|
|
347
|
+
if (toolName === 'grep') return 'search';
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function filePathFromTool(part) {
|
|
352
|
+
const input = part.state?.input;
|
|
353
|
+
if (!input || typeof input !== 'object') return undefined;
|
|
354
|
+
return input.filePath || input.path || input.pattern;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function oneLine(value, max) {
|
|
358
|
+
return shorten(String(value ?? '').replace(/\s+/g, ' ').trim(), max);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function resolveIsoTraceCli() {
|
|
362
|
+
const pkgJsonPath = require.resolve('@razroo/iso-trace/package.json');
|
|
363
|
+
return join(dirname(pkgJsonPath), 'dist/cli.js');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function passthroughIsoTrace(rawArgs) {
|
|
367
|
+
const cliPath = resolveIsoTraceCli();
|
|
368
|
+
const result = spawnSync(process.execPath, [cliPath, ...rawArgs], {
|
|
369
|
+
stdio: 'inherit',
|
|
370
|
+
cwd: PROJECT_DIR,
|
|
371
|
+
env: process.env,
|
|
372
|
+
});
|
|
373
|
+
return result.status ?? 1;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function main() {
|
|
377
|
+
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
378
|
+
console.log(USAGE);
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (cmd === 'list') {
|
|
383
|
+
const { opts } = parseFilters(args);
|
|
384
|
+
if (opts.help) {
|
|
385
|
+
console.log(USAGE);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
if (opts.error) {
|
|
389
|
+
console.error(`job-forge trace:list: ${opts.error}`);
|
|
390
|
+
return 2;
|
|
391
|
+
}
|
|
392
|
+
const refs = await discoverOpenCodeRefs(opts);
|
|
393
|
+
if (opts.json) {
|
|
394
|
+
console.log(JSON.stringify(refs, null, 2));
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
if (refs.length === 0) {
|
|
398
|
+
console.error('job-forge trace:list: no OpenCode sessions found for this project');
|
|
399
|
+
return 2;
|
|
400
|
+
}
|
|
401
|
+
printSessionTable(refs);
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (cmd === 'stats') {
|
|
406
|
+
const { opts, positional } = parseFilters(args);
|
|
407
|
+
if (opts.help) {
|
|
408
|
+
console.log(USAGE);
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
if (opts.error) {
|
|
412
|
+
console.error(`job-forge trace:stats: ${opts.error}`);
|
|
413
|
+
return 2;
|
|
414
|
+
}
|
|
415
|
+
const refs = await discoverOpenCodeRefs(opts);
|
|
416
|
+
const selected = positional.length === 0
|
|
417
|
+
? refs
|
|
418
|
+
: positional.map((id) => {
|
|
419
|
+
const ref = findSessionById(refs, id);
|
|
420
|
+
if (!ref) throw new Error(`job-forge trace:stats: no OpenCode session matches "${id}"`);
|
|
421
|
+
return ref;
|
|
422
|
+
});
|
|
423
|
+
const result = computeOpenCodeStats(selected);
|
|
424
|
+
if (opts.json) {
|
|
425
|
+
console.log(JSON.stringify(result, null, 2));
|
|
426
|
+
} else {
|
|
427
|
+
printStats(result);
|
|
428
|
+
}
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (cmd === 'show') {
|
|
433
|
+
const { opts, positional } = parseShowArgs(args);
|
|
434
|
+
if (opts.help) {
|
|
435
|
+
console.log(USAGE);
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
if (opts.error) {
|
|
439
|
+
console.error(`job-forge trace:show: ${opts.error}`);
|
|
440
|
+
return 2;
|
|
441
|
+
}
|
|
442
|
+
if (opts.grep instanceof Error) {
|
|
443
|
+
console.error(opts.grep.message);
|
|
444
|
+
return 2;
|
|
445
|
+
}
|
|
446
|
+
if (positional.length === 0) {
|
|
447
|
+
console.error('job-forge trace:show: missing <id-or-prefix>');
|
|
448
|
+
return 2;
|
|
449
|
+
}
|
|
450
|
+
const refs = await discoverOpenCodeRefs({ cwd: PROJECT_DIR, since: undefined });
|
|
451
|
+
const ref = findSessionById(refs, positional[0]);
|
|
452
|
+
if (!ref) {
|
|
453
|
+
console.error(`job-forge trace:show: no OpenCode session matches "${positional[0]}"`);
|
|
454
|
+
return 2;
|
|
455
|
+
}
|
|
456
|
+
printOpenCodeSession(ref, opts);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return passthroughIsoTrace([cmd, ...args]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
main()
|
|
464
|
+
.then((code) => process.exit(code))
|
|
465
|
+
.catch((error) => {
|
|
466
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
467
|
+
console.error(message);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
});
|