@raquezha/notrace 0.0.4 → 0.0.6
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 +15 -0
- package/README.md +123 -23
- package/assets/notrace-logo.svg +20 -0
- package/assets/notrace-mark.svg +18 -0
- package/assets/notrace-wordmark.svg +4 -0
- package/dist/notrace/adapters.d.ts +32 -0
- package/dist/notrace/adapters.js +83 -0
- package/dist/notrace/index.d.ts +2 -0
- package/dist/notrace/index.js +295 -0
- package/dist/notrace/renderer.d.ts +4 -0
- package/dist/notrace/renderer.js +605 -0
- package/dist/notrace/types.d.ts +93 -0
- package/dist/notrace/types.js +1 -0
- package/extensions/notrace/adapters.ts +88 -0
- package/extensions/notrace/index.ts +359 -0
- package/extensions/notrace/renderer.ts +616 -0
- package/extensions/notrace/types.ts +108 -0
- package/package.json +4 -3
- package/templates/README.md +24 -0
- package/templates/dashboard.sample.html +361 -0
- package/templates/dashboard.sample.json +113 -0
- package/templates/notrace-logo.preview.png +0 -0
- package/templates/render-samples.mjs +44 -0
- package/templates/session.sample.html +431 -0
- package/templates/session.sample.json +127 -0
- package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
- package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
- package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
- package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +426 -0
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +425 -0
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +426 -0
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
- package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
- package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
- package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
- package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
- package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
- package/tsconfig.json +1 -1
- package/dist/notrace.d.ts +0 -9
- package/dist/notrace.js +0 -785
- package/extensions/notrace.ts +0 -802
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @raquezha/notrace
|
|
2
2
|
|
|
3
|
+
## 0.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d349d36: Refresh the notrace UI and sample session rendering separately from the antigravity billing fix.
|
|
8
|
+
- 51fda83: fix: preserve assistant toolCall blocks in noheadroom compression and expose notrace failure metadata
|
|
9
|
+
- 7afa746: Package updates for antigravity, norpiv, and notrace.
|
|
10
|
+
|
|
11
|
+
## 0.0.5
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 6eac69d: Relocate core configuration files (`mindsets.json`, `settings.json`, `AGENTS.md`) to a dedicated `config/` directory for better maintainability and a cleaner repository root. Updated `bootstrap.sh` and shell integration to support the new layout.
|
|
16
|
+
- c19a93a: Add a machine-readable `notrace.json` run record alongside the existing HTML report to normalize captured session/task metadata, activity metrics, and evidence for future retrospective and comparison workflows.
|
|
17
|
+
|
|
3
18
|
## 0.0.4
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,48 +1,148 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/notrace-logo.svg" alt="notrace logo" width="240" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# notrace
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
**Traces in, lessons out.**
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
`notrace` is a local-first retrospective engine for the Pi Coding Agent.
|
|
10
|
+
It captures session evidence, writes a versioned `notrace.json` run record, renders a human-readable HTML report, and supports review/compare flows for workflow R&D.
|
|
6
11
|
|
|
7
|
-
##
|
|
12
|
+
## What notrace owns
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- **Safer defaults**: Secret-key/value redaction, bounded payload sizes, metadata-only mode, private file permissions, and `.workflow`-confined report writes
|
|
14
|
+
When enabled, `notrace` is the durable retrospective layer for a session.
|
|
15
|
+
It aggregates:
|
|
16
|
+
- core Pi session telemetry
|
|
17
|
+
- workflow/task context
|
|
18
|
+
- optional dynamic extension telemetry
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
Today, Pi is the first harness adapter.
|
|
21
|
+
The canonical run schema is designed so other harness adapters can be added later, but multi-harness support is not implemented in this package yet.
|
|
17
22
|
|
|
23
|
+
## What notrace does not own
|
|
24
|
+
|
|
25
|
+
`notrace` is **not**:
|
|
26
|
+
- the live Pi footer
|
|
27
|
+
- the Pi resume/session-switch UX
|
|
28
|
+
- a scraper of terminal status strings
|
|
29
|
+
|
|
30
|
+
Live footer output, resume hints, and extension footer badges may appear near `notrace` output during shutdown, but they are separate producers.
|
|
31
|
+
|
|
32
|
+
## Retrospective spine
|
|
33
|
+
|
|
34
|
+
1. **Capture evidence**: `notrace.json`
|
|
35
|
+
2. **Inspect**: `notrace.html`
|
|
36
|
+
3. **Review outcome**: `notrace.review.json`
|
|
37
|
+
4. **Compare attempts**: `compare:notrace`
|
|
38
|
+
|
|
39
|
+
## Storage
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
.notrace/
|
|
43
|
+
index.json
|
|
44
|
+
index.html
|
|
45
|
+
sessions/
|
|
46
|
+
<session-id>/
|
|
47
|
+
notrace.json
|
|
48
|
+
notrace.html
|
|
49
|
+
notrace.review.json
|
|
18
50
|
```
|
|
19
|
-
🔍 [notrace] Observability report generated:
|
|
20
|
-
📂 file:///path/to/.workflow/tasks/my-task/notrace.html
|
|
21
|
-
```
|
|
22
51
|
|
|
23
|
-
##
|
|
52
|
+
## Canonical run model
|
|
53
|
+
|
|
54
|
+
Generated `notrace.json` is the source of truth for runtime output, HTML rendering, and downstream tooling.
|
|
55
|
+
The record is versioned and centers on:
|
|
56
|
+
- `kind`
|
|
57
|
+
- `schemaVersion`
|
|
58
|
+
- `traceId`
|
|
59
|
+
- `repository`
|
|
60
|
+
- `session`
|
|
61
|
+
- `task`
|
|
62
|
+
- `captureMode`
|
|
63
|
+
- `conditions`
|
|
64
|
+
- `activity`
|
|
65
|
+
- `telemetry`
|
|
66
|
+
- `events`
|
|
67
|
+
|
|
68
|
+
Key rule:
|
|
69
|
+
- **consumed tokens** and **saved tokens** are separate metric families
|
|
70
|
+
- optimization telemetry belongs under `telemetry.extensions.*`
|
|
71
|
+
- presentation-only UI strings are not canonical evidence
|
|
72
|
+
|
|
73
|
+
## Dynamic extension telemetry
|
|
74
|
+
|
|
75
|
+
`notrace` can include optional structured telemetry from dynamic extensions.
|
|
76
|
+
Current first target is `noheadroom`.
|
|
77
|
+
|
|
78
|
+
If an extension is absent, `notrace` should still succeed.
|
|
79
|
+
If an extension is present, it can contribute a structured summary such as:
|
|
80
|
+
- loaded / enabled / active state
|
|
81
|
+
- optimization attempts
|
|
82
|
+
- tokens saved
|
|
83
|
+
- last applied compression summary
|
|
84
|
+
|
|
85
|
+
## Capture modes
|
|
86
|
+
|
|
87
|
+
Default capture mode is **full**.
|
|
24
88
|
|
|
25
89
|
```bash
|
|
26
|
-
# Load directly
|
|
27
90
|
pi --extension ./packages/notrace
|
|
91
|
+
```
|
|
28
92
|
|
|
29
|
-
|
|
30
|
-
|
|
93
|
+
Optional modes:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
NOTRACE_CAPTURE=redacted pi --extension ./packages/notrace
|
|
97
|
+
NOTRACE_CAPTURE=metadata pi --extension ./packages/notrace
|
|
98
|
+
NOTRACE_CAPTURE=full pi --extension ./packages/notrace
|
|
31
99
|
```
|
|
32
100
|
|
|
33
|
-
|
|
101
|
+
Mode meanings:
|
|
102
|
+
- `full`: full captured payloads; best for local debugging; highest sensitivity
|
|
103
|
+
- `redacted`: captured payloads with common secret-like values redacted
|
|
104
|
+
- `metadata`: minimal capture, no prompt/tool bodies
|
|
105
|
+
|
|
106
|
+
**Security warning:** even redacted reports can contain sensitive prompts, tool payloads, local paths, and outputs. Do not publish generated reports.
|
|
107
|
+
|
|
108
|
+
## Review
|
|
34
109
|
|
|
35
110
|
```bash
|
|
36
|
-
npm
|
|
111
|
+
npm run review:notrace -- \
|
|
112
|
+
.notrace/sessions/<id>/notrace.json \
|
|
113
|
+
--outcome partial \
|
|
114
|
+
--friction high \
|
|
115
|
+
--lesson "Headroom reduced tokens but needed manual steering." \
|
|
116
|
+
--next-change "Try same task with RepoScry enabled."
|
|
37
117
|
```
|
|
38
118
|
|
|
39
|
-
|
|
119
|
+
Review fields:
|
|
120
|
+
- `outcome`: `success`, `partial`, `failed`, `abandoned`, `inconclusive`
|
|
121
|
+
- `friction`: `low`, `medium`, `high`
|
|
122
|
+
- `lesson`
|
|
123
|
+
- `nextChange`
|
|
40
124
|
|
|
41
|
-
|
|
125
|
+
## Compare
|
|
42
126
|
|
|
43
127
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
128
|
+
npm run compare:notrace -- \
|
|
129
|
+
.notrace/sessions/<baseline-id>/notrace.json \
|
|
130
|
+
.notrace/sessions/<candidate-id>/notrace.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Templates
|
|
134
|
+
|
|
135
|
+
HTML source-of-truth lives in `templates/`:
|
|
136
|
+
- `dashboard.sample.json`
|
|
137
|
+
- `session.sample.json`
|
|
138
|
+
- `dashboard.sample.html`
|
|
139
|
+
- `session.sample.html`
|
|
140
|
+
|
|
141
|
+
Refresh previews after renderer changes:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
cd packages/notrace
|
|
145
|
+
npm run render:samples
|
|
46
146
|
```
|
|
47
147
|
|
|
48
148
|
## Build
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg viewBox="0 0 420 138" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="notrace">
|
|
2
|
+
<title>notrace logo</title>
|
|
3
|
+
<desc>Signal icon above and slightly overlapping the notrace wordmark.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="fadeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
6
|
+
<stop offset="0%" stop-color="#E2754A"/>
|
|
7
|
+
<stop offset="100%" stop-color="#EDE2D2"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<g id="trace-icon" transform="translate(1 -5) scale(0.93)">
|
|
11
|
+
<path d="M6,50 C16,18 26,18 36,50 C46,82 54,82 60,50 C64,30 68,30 71,50"
|
|
12
|
+
fill="none" stroke="url(#fadeGrad)" stroke-width="4" stroke-linecap="round"/>
|
|
13
|
+
<line x1="74" y1="50" x2="79" y2="50" stroke="#D9C9B5" stroke-width="4" stroke-linecap="round" stroke-opacity="0.6"/>
|
|
14
|
+
<circle cx="85" cy="50" r="2.2" fill="#D9C9B5" opacity="0.5"/>
|
|
15
|
+
<circle cx="90" cy="50" r="1.4" fill="#EDE2D2" opacity="0.32"/>
|
|
16
|
+
<circle cx="94" cy="50" r="0.9" fill="#EDE2D2" opacity="0.15"/>
|
|
17
|
+
</g>
|
|
18
|
+
<text x="0" y="114" fill="#ECE3DA" style="fill:#ECE3DA" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="96" font-weight="900" letter-spacing="-7">no</text>
|
|
19
|
+
<text x="82" y="114" fill="#D88462" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="96" font-weight="900" letter-spacing="-7">trace</text>
|
|
20
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img">
|
|
2
|
+
<title>notrace logo mark</title>
|
|
3
|
+
<desc>A wave that smooths into a flat line, then fades into dots — color shifts from trace orange to no cream as it dissolves.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="fadeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
6
|
+
<stop offset="0%" stop-color="#E2754A"/>
|
|
7
|
+
<stop offset="100%" stop-color="#EDE2D2"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<g id="trace-icon">
|
|
11
|
+
<path d="M6,50 C16,18 26,18 36,50 C46,82 54,82 60,50 C64,30 68,30 71,50"
|
|
12
|
+
fill="none" stroke="url(#fadeGrad)" stroke-width="4" stroke-linecap="round"/>
|
|
13
|
+
<line x1="74" y1="50" x2="79" y2="50" stroke="#D9C9B5" stroke-width="4" stroke-linecap="round" stroke-opacity="0.6"/>
|
|
14
|
+
<circle cx="85" cy="50" r="2.2" fill="#D9C9B5" opacity="0.5"/>
|
|
15
|
+
<circle cx="90" cy="50" r="1.4" fill="#EDE2D2" opacity="0.32"/>
|
|
16
|
+
<circle cx="94" cy="50" r="0.9" fill="#EDE2D2" opacity="0.15"/>
|
|
17
|
+
</g>
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg viewBox="0 0 420 96" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="notrace wordmark">
|
|
2
|
+
<text x="0" y="76" fill="#ECE3DA" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="96" font-weight="900" letter-spacing="-7">no</text>
|
|
3
|
+
<text x="82" y="76" fill="#D88462" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="96" font-weight="900" letter-spacing="-7">trace</text>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WorkflowContext } from "./types.js";
|
|
2
|
+
export interface WorkflowAdapter {
|
|
3
|
+
name: string;
|
|
4
|
+
detect(cwd: string): boolean;
|
|
5
|
+
getContext(cwd: string): WorkflowContext | null;
|
|
6
|
+
attach(context: WorkflowContext, artifacts: {
|
|
7
|
+
html: string;
|
|
8
|
+
record: string;
|
|
9
|
+
}): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class NorpivAdapter implements WorkflowAdapter {
|
|
12
|
+
name: string;
|
|
13
|
+
detect(cwd: string): boolean;
|
|
14
|
+
getContext(cwd: string): WorkflowContext | null;
|
|
15
|
+
attach(context: WorkflowContext, artifacts: {
|
|
16
|
+
html: string;
|
|
17
|
+
record: string;
|
|
18
|
+
}): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class ResearchAdapter implements WorkflowAdapter {
|
|
21
|
+
name: string;
|
|
22
|
+
detect(cwd: string): boolean;
|
|
23
|
+
getContext(cwd: string): WorkflowContext | null;
|
|
24
|
+
attach(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare class GenericAdapter implements WorkflowAdapter {
|
|
27
|
+
name: string;
|
|
28
|
+
detect(): boolean;
|
|
29
|
+
getContext(): null;
|
|
30
|
+
attach(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function getActiveAdapter(cwd: string): WorkflowAdapter;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
function appendWorkLogEntry(taskDir, message) {
|
|
4
|
+
const workMd = path.join(taskDir, "WORK.md");
|
|
5
|
+
if (!existsSync(workMd))
|
|
6
|
+
return;
|
|
7
|
+
try {
|
|
8
|
+
const text = readFileSync(workMd, "utf-8");
|
|
9
|
+
const entry = `- ${new Date().toISOString()}: ${message}`;
|
|
10
|
+
if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
|
|
11
|
+
writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const lines = text.split("\n");
|
|
15
|
+
const logIndex = lines.findIndex(l => /^(## )?\[LOG\]\s*$/.test(l));
|
|
16
|
+
let nextSection = lines.length;
|
|
17
|
+
for (let i = logIndex + 1; i < lines.length; i++) {
|
|
18
|
+
if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
|
|
19
|
+
nextSection = i;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const before = lines.slice(0, nextSection);
|
|
24
|
+
const after = lines.slice(nextSection);
|
|
25
|
+
before.push(entry);
|
|
26
|
+
writeFileSync(workMd, `${before.join("\n")}\n${after.join("\n")}`);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
export class NorpivAdapter {
|
|
31
|
+
name = "norpiv";
|
|
32
|
+
detect(cwd) {
|
|
33
|
+
return existsSync(path.join(cwd, ".workflow", "active_task.json"));
|
|
34
|
+
}
|
|
35
|
+
getContext(cwd) {
|
|
36
|
+
try {
|
|
37
|
+
const workflowDir = path.join(cwd, ".workflow");
|
|
38
|
+
const content = JSON.parse(readFileSync(path.join(workflowDir, "active_task.json"), "utf-8"));
|
|
39
|
+
const taskPath = content.taskPath || (content.active_task ? path.join("tasks", content.active_task) : null);
|
|
40
|
+
return {
|
|
41
|
+
workflow: this.name,
|
|
42
|
+
taskId: content.active_task || "unknown",
|
|
43
|
+
taskPath,
|
|
44
|
+
taskDir: taskPath ? path.resolve(cwd, taskPath) : null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
attach(context, artifacts) {
|
|
52
|
+
if (!context.taskDir)
|
|
53
|
+
return;
|
|
54
|
+
appendWorkLogEntry(context.taskDir, `notrace retrospective: ${artifacts.html}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class ResearchAdapter {
|
|
58
|
+
name = "research";
|
|
59
|
+
detect(cwd) {
|
|
60
|
+
return existsSync(path.join(cwd, ".git")) && !existsSync(path.join(cwd, ".workflow", "active_task.json"));
|
|
61
|
+
}
|
|
62
|
+
getContext(cwd) {
|
|
63
|
+
try {
|
|
64
|
+
const head = readFileSync(path.join(cwd, ".git", "HEAD"), "utf-8");
|
|
65
|
+
const branch = head.split("refs/heads/")[1]?.trim() || "main";
|
|
66
|
+
return { workflow: this.name, taskId: `branch:${branch}`, taskPath: null, taskDir: null };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
attach() { }
|
|
73
|
+
}
|
|
74
|
+
export class GenericAdapter {
|
|
75
|
+
name = "generic";
|
|
76
|
+
detect() { return true; }
|
|
77
|
+
getContext() { return null; }
|
|
78
|
+
attach() { }
|
|
79
|
+
}
|
|
80
|
+
const ADAPTERS = [new NorpivAdapter(), new ResearchAdapter(), new GenericAdapter()];
|
|
81
|
+
export function getActiveAdapter(cwd) {
|
|
82
|
+
return ADAPTERS.find(a => a.detect(cwd)) || new GenericAdapter();
|
|
83
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getActiveAdapter } from "./adapters.js";
|
|
4
|
+
import { generateHtmlReport, generateDashboardHtml } from "./renderer.js";
|
|
5
|
+
const REDACTED = "[REDACTED by notrace]";
|
|
6
|
+
const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|AKIA[0-9A-Z]{16})/gi;
|
|
7
|
+
const TELEMETRY_CHANNEL = "notrace.telemetry.extension";
|
|
8
|
+
const SCHEMA_VERSION = 2;
|
|
9
|
+
let currentMode = "full";
|
|
10
|
+
function getInitialMode() {
|
|
11
|
+
const env = process.env.NOTRACE_CAPTURE?.toLowerCase();
|
|
12
|
+
if (env === "metadata" || env === "redacted")
|
|
13
|
+
return env;
|
|
14
|
+
return "full";
|
|
15
|
+
}
|
|
16
|
+
const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
|
|
17
|
+
function isSensitiveKey(key) {
|
|
18
|
+
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
19
|
+
if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized))
|
|
20
|
+
return false;
|
|
21
|
+
return SENSITIVE_KEY_RE.test(normalized);
|
|
22
|
+
}
|
|
23
|
+
function sanitizeTraceValue(value) {
|
|
24
|
+
if (currentMode === "full")
|
|
25
|
+
return value;
|
|
26
|
+
if (value == null || typeof value !== "object") {
|
|
27
|
+
return typeof value === "string" ? value.replace(SENSITIVE_VALUE_RE, REDACTED).slice(0, 10000) : value;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return value.slice(0, 100).map(sanitizeTraceValue);
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, v] of Object.entries(value).slice(0, 100))
|
|
33
|
+
out[k] = isSensitiveKey(k) ? REDACTED : sanitizeTraceValue(v);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function asNumber(value) {
|
|
37
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
38
|
+
}
|
|
39
|
+
function normalizeUsage(raw) {
|
|
40
|
+
const usage = (raw && typeof raw === "object" ? raw : {});
|
|
41
|
+
return {
|
|
42
|
+
inputTokens: asNumber(usage.inputTokens ?? usage.input),
|
|
43
|
+
outputTokens: asNumber(usage.outputTokens ?? usage.output),
|
|
44
|
+
cacheReadTokens: asNumber(usage.cacheReadTokens ?? usage.cacheRead),
|
|
45
|
+
cacheWriteTokens: asNumber(usage.cacheWriteTokens ?? usage.cacheWrite),
|
|
46
|
+
totalTokens: asNumber(usage.totalTokens),
|
|
47
|
+
totalCostUsd: asNumber(usage.cost?.total),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function collectActivity(events, startedAt, endedAt) {
|
|
51
|
+
const activity = {
|
|
52
|
+
turnCount: 0,
|
|
53
|
+
llmCallCount: 0,
|
|
54
|
+
toolCallCount: 0,
|
|
55
|
+
toolErrorCount: 0,
|
|
56
|
+
durationMs: Math.max(0, endedAt - startedAt),
|
|
57
|
+
totals: {
|
|
58
|
+
inputTokens: 0,
|
|
59
|
+
outputTokens: 0,
|
|
60
|
+
cacheReadTokens: 0,
|
|
61
|
+
cacheWriteTokens: 0,
|
|
62
|
+
totalTokens: 0,
|
|
63
|
+
totalCostUsd: 0,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
for (const e of events) {
|
|
67
|
+
if (e.type === "turn_start")
|
|
68
|
+
activity.turnCount++;
|
|
69
|
+
if (e.type === "tool_start")
|
|
70
|
+
activity.toolCallCount++;
|
|
71
|
+
if (e.type === "tool_end" && e.isError)
|
|
72
|
+
activity.toolErrorCount++;
|
|
73
|
+
if (e.type === "llm_completion") {
|
|
74
|
+
activity.llmCallCount++;
|
|
75
|
+
const usage = normalizeUsage(e.usage);
|
|
76
|
+
activity.totals.inputTokens += usage.inputTokens;
|
|
77
|
+
activity.totals.outputTokens += usage.outputTokens;
|
|
78
|
+
activity.totals.cacheReadTokens += usage.cacheReadTokens;
|
|
79
|
+
activity.totals.cacheWriteTokens += usage.cacheWriteTokens;
|
|
80
|
+
activity.totals.totalTokens += usage.totalTokens;
|
|
81
|
+
activity.totals.totalCostUsd += usage.totalCostUsd;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return activity;
|
|
85
|
+
}
|
|
86
|
+
function buildConditions(events, telemetry) {
|
|
87
|
+
const models = new Set();
|
|
88
|
+
const providers = new Set();
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (event.type !== "llm_completion")
|
|
91
|
+
continue;
|
|
92
|
+
if (typeof event.model === "string" && event.model)
|
|
93
|
+
models.add(event.model);
|
|
94
|
+
if (typeof event.provider === "string" && event.provider)
|
|
95
|
+
providers.add(event.provider);
|
|
96
|
+
}
|
|
97
|
+
const extensions = ["notrace", ...Object.keys(telemetry).sort()];
|
|
98
|
+
return {
|
|
99
|
+
harness: {
|
|
100
|
+
name: "pi",
|
|
101
|
+
adapter: "pi-session-hooks",
|
|
102
|
+
version: null,
|
|
103
|
+
},
|
|
104
|
+
models: [...models],
|
|
105
|
+
providers: [...providers],
|
|
106
|
+
extensions,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function toTaskInfo(context) {
|
|
110
|
+
if (!context)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
workflow: context.workflow,
|
|
114
|
+
id: context.taskId,
|
|
115
|
+
path: context.taskPath,
|
|
116
|
+
dir: context.taskDir,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createIndexEntry(record, cwd, htmlPath, recordPath) {
|
|
120
|
+
return {
|
|
121
|
+
sessionId: record.traceId,
|
|
122
|
+
startedAt: record.session.startedAt,
|
|
123
|
+
endedAt: record.session.endedAt,
|
|
124
|
+
captureMode: record.captureMode,
|
|
125
|
+
task: record.task,
|
|
126
|
+
conditions: record.conditions,
|
|
127
|
+
activity: record.activity,
|
|
128
|
+
artifacts: {
|
|
129
|
+
html: path.relative(cwd, htmlPath),
|
|
130
|
+
record: path.relative(cwd, recordPath),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function normalizeTelemetryPayload(raw) {
|
|
135
|
+
if (!raw || typeof raw !== "object")
|
|
136
|
+
return null;
|
|
137
|
+
const payload = raw;
|
|
138
|
+
if (typeof payload.extension !== "string" || !payload.extension.trim())
|
|
139
|
+
return null;
|
|
140
|
+
const status = payload.status === "absent" ||
|
|
141
|
+
payload.status === "loaded-disabled" ||
|
|
142
|
+
payload.status === "loaded-inactive" ||
|
|
143
|
+
payload.status === "active" ||
|
|
144
|
+
payload.status === "unknown"
|
|
145
|
+
? payload.status
|
|
146
|
+
: "unknown";
|
|
147
|
+
return {
|
|
148
|
+
extension: payload.extension,
|
|
149
|
+
telemetry: {
|
|
150
|
+
loaded: payload.loaded !== false,
|
|
151
|
+
enabled: typeof payload.enabled === "boolean" ? payload.enabled : null,
|
|
152
|
+
active: typeof payload.active === "boolean" ? payload.active : null,
|
|
153
|
+
status,
|
|
154
|
+
summary: typeof payload.summary === "string" ? payload.summary : null,
|
|
155
|
+
details: payload.details && typeof payload.details === "object" ? payload.details : {},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export default function (pi) {
|
|
160
|
+
const events = [];
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
let traceId = "";
|
|
163
|
+
let activeLlmPayload = null;
|
|
164
|
+
let shutdownReason = null;
|
|
165
|
+
const extensionTelemetry = new Map();
|
|
166
|
+
currentMode = getInitialMode();
|
|
167
|
+
if (typeof pi.events?.on === "function") {
|
|
168
|
+
pi.events.on(TELEMETRY_CHANNEL, (raw) => {
|
|
169
|
+
const normalized = normalizeTelemetryPayload(raw);
|
|
170
|
+
if (!normalized)
|
|
171
|
+
return;
|
|
172
|
+
extensionTelemetry.set(normalized.extension, normalized.telemetry);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
pi.registerCommand("notrace", {
|
|
176
|
+
description: "Change notrace capture mode (full | redacted | metadata)",
|
|
177
|
+
handler: async (args, ctx) => {
|
|
178
|
+
const mode = args?.trim().toLowerCase();
|
|
179
|
+
if (mode === "full" || mode === "redacted" || mode === "metadata") {
|
|
180
|
+
currentMode = mode;
|
|
181
|
+
ctx.ui.notify(`notrace capture mode set to: ${currentMode}`, "info");
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
ctx.ui.notify(`Current notrace mode: ${currentMode}. Usage: /notrace [full|redacted|metadata]`, "info");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
189
|
+
traceId = ctx.sessionManager.getSessionId() || `s-${Date.now()}`;
|
|
190
|
+
events.push({ type: "session_start", timestamp: Date.now() });
|
|
191
|
+
});
|
|
192
|
+
pi.on("turn_start", async () => events.push({ type: "turn_start", timestamp: Date.now() }));
|
|
193
|
+
pi.on("tool_execution_start", async (e) => {
|
|
194
|
+
events.push({ type: "tool_start", toolName: e.toolName, args: sanitizeTraceValue(e.args), timestamp: Date.now() });
|
|
195
|
+
});
|
|
196
|
+
pi.on("tool_execution_end", async (e) => {
|
|
197
|
+
events.push({ type: "tool_end", toolName: e.toolName, result: sanitizeTraceValue(e.result), isError: e.isError, timestamp: Date.now() });
|
|
198
|
+
});
|
|
199
|
+
pi.on("before_provider_request", async (e) => {
|
|
200
|
+
activeLlmPayload = sanitizeTraceValue(e.payload);
|
|
201
|
+
});
|
|
202
|
+
pi.on("message_end", async (e) => {
|
|
203
|
+
if (e.message.role === "assistant") {
|
|
204
|
+
events.push({
|
|
205
|
+
type: "llm_completion",
|
|
206
|
+
model: e.message.model,
|
|
207
|
+
provider: e.message.provider,
|
|
208
|
+
inputPayload: activeLlmPayload,
|
|
209
|
+
outputContent: sanitizeTraceValue(e.message.content),
|
|
210
|
+
usage: e.message.usage,
|
|
211
|
+
stopReason: typeof e.message.stopReason === "string" ? e.message.stopReason : undefined,
|
|
212
|
+
errorMessage: typeof e.message.errorMessage === "string" ? sanitizeTraceValue(e.message.errorMessage) : undefined,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
activeLlmPayload = null;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
pi.on("session_shutdown", async (e, ctx) => {
|
|
219
|
+
shutdownReason = typeof e?.reason === "string" ? e.reason : null;
|
|
220
|
+
const endedAt = Date.now();
|
|
221
|
+
const adapter = getActiveAdapter(ctx.cwd);
|
|
222
|
+
const context = adapter.getContext(ctx.cwd);
|
|
223
|
+
const notraceDir = path.resolve(ctx.cwd, ".notrace");
|
|
224
|
+
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
225
|
+
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
226
|
+
const repositoryName = path.basename(ctx.cwd);
|
|
227
|
+
const recordPath = path.join(outputDir, "notrace.json");
|
|
228
|
+
let mergedEvents = events;
|
|
229
|
+
let originalStartedAt = startTime;
|
|
230
|
+
let originalTask = null;
|
|
231
|
+
if (existsSync(recordPath)) {
|
|
232
|
+
try {
|
|
233
|
+
const oldRecord = JSON.parse(readFileSync(recordPath, "utf-8"));
|
|
234
|
+
if (Array.isArray(oldRecord.events)) {
|
|
235
|
+
mergedEvents = [...oldRecord.events, ...events];
|
|
236
|
+
}
|
|
237
|
+
if (oldRecord.session?.startedAt) {
|
|
238
|
+
originalStartedAt = new Date(oldRecord.session.startedAt).getTime();
|
|
239
|
+
}
|
|
240
|
+
if (oldRecord.task) {
|
|
241
|
+
originalTask = oldRecord.task;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
// ignore parse errors
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const activity = collectActivity(mergedEvents, originalStartedAt, endedAt);
|
|
249
|
+
// Do not index purely empty ghost sessions
|
|
250
|
+
const isGhostSession = activity.llmCallCount === 0 && activity.toolCallCount === 0 && activity.totals.totalTokens === 0;
|
|
251
|
+
const telemetry = Object.fromEntries([...extensionTelemetry.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
252
|
+
const record = {
|
|
253
|
+
kind: "notrace-run",
|
|
254
|
+
schemaVersion: SCHEMA_VERSION,
|
|
255
|
+
traceId: finalTraceId,
|
|
256
|
+
repository: {
|
|
257
|
+
name: repositoryName,
|
|
258
|
+
cwd: ctx.cwd,
|
|
259
|
+
},
|
|
260
|
+
session: {
|
|
261
|
+
id: finalTraceId,
|
|
262
|
+
startedAt: new Date(originalStartedAt).toISOString(),
|
|
263
|
+
endedAt: new Date(endedAt).toISOString(),
|
|
264
|
+
durationMs: activity.durationMs,
|
|
265
|
+
shutdownReason,
|
|
266
|
+
},
|
|
267
|
+
task: toTaskInfo(context) || originalTask,
|
|
268
|
+
captureMode: currentMode,
|
|
269
|
+
conditions: buildConditions(mergedEvents, telemetry),
|
|
270
|
+
activity,
|
|
271
|
+
telemetry: { extensions: telemetry },
|
|
272
|
+
events: mergedEvents,
|
|
273
|
+
};
|
|
274
|
+
const html = generateHtmlReport(record);
|
|
275
|
+
mkdirSync(outputDir, { recursive: true });
|
|
276
|
+
const htmlPath = path.join(outputDir, "notrace.html");
|
|
277
|
+
writeFileSync(htmlPath, html);
|
|
278
|
+
writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
279
|
+
const indexPath = path.join(notraceDir, "index.json");
|
|
280
|
+
const existing = existsSync(indexPath) ? JSON.parse(readFileSync(indexPath, "utf-8")) : { repositoryName, sessions: [] };
|
|
281
|
+
let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s) => s.sessionId !== finalTraceId) : [];
|
|
282
|
+
if (!isGhostSession) {
|
|
283
|
+
sessions.push(createIndexEntry(record, ctx.cwd, htmlPath, recordPath));
|
|
284
|
+
}
|
|
285
|
+
writeFileSync(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
286
|
+
writeFileSync(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
287
|
+
if (context) {
|
|
288
|
+
adapter.attach(context, {
|
|
289
|
+
html: path.relative(ctx.cwd, htmlPath),
|
|
290
|
+
record: path.relative(ctx.cwd, recordPath)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
console.log(`\n\x1b[1m\x1b[38;5;208m[notrace] Session Retrospective: file://${htmlPath}\x1b[0m\n`);
|
|
294
|
+
});
|
|
295
|
+
}
|