@harnessforce/claude-code-plugin 0.2.0 → 0.3.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +19 -2
- package/commands/setup.md +9 -11
- package/hooks/hooks.json +45 -1
- package/package.json +1 -1
- package/scripts/harnessforce-hook-main.cjs +181 -67
- package/skills/using-harnessforce/SKILL.md +3 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ and does not bundle the CLI (`/harnessforce:setup` installs `@harnessforce/cli`
|
|
|
11
11
|
| Part | Purpose |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| `SessionStart` and `UserPromptSubmit` hooks | Register each session and its config snapshot with Harnessforce |
|
|
14
|
+
| `CwdChanged`, `Stop` and `PostToolUse` (`Write` and `Edit`) hooks | Record the repository and branch of the Git working trees a session works in, and send them at the end of a turn in which they changed |
|
|
14
15
|
| `PostToolUse`, `PostToolUseFailure`, `UserPromptExpansion`, `PermissionRequest`, `PostCompact` and `SessionEnd` hooks | Count the configured skills, commands, subagents and MCP servers a session uses, and send the session usage summary |
|
|
15
16
|
| MCP server `harnessforce` (`<connection URL>/mcp`, HTTP) | Tools to start a run, read an Issue, and complete a run |
|
|
16
17
|
| Skill `using-harnessforce` | What Harnessforce records and does not collect, which hook sends what, and when to use the other skills. The `SessionStart` hook injects its body into the context of every session (the build embeds the same text into the hook and the CLI) |
|
|
@@ -72,8 +73,8 @@ running. `harnessforce init` turns on automatic updates for the `harnessforce-ag
|
|
|
72
73
|
The `harnessforce` plugin registers each Claude Code session with Harnessforce so that runs can be
|
|
73
74
|
linked to issues. On `SessionStart` (`startup`, `clear`, `fork`) it sends the session ID and the git
|
|
74
75
|
repository, branch and commit of `cwd` to `POST <HARNESSFORCE_ENDPOINT>/v1/sessions`. On the first
|
|
75
|
-
prompt it sends the same registration again with the prompt ID.
|
|
76
|
-
contents are
|
|
76
|
+
prompt it sends the same registration again with the prompt ID. The hook never sends prompts,
|
|
77
|
+
responses or file contents; they are sent only through the OTel settings you choose.
|
|
77
78
|
|
|
78
79
|
- For an organization, set `HARNESSFORCE_ENDPOINT` (the ingest base URL, `https:`) and
|
|
79
80
|
`HARNESSFORCE_INGEST_KEY` (a Workspace ingest key) in
|
|
@@ -94,6 +95,22 @@ contents are never sent.
|
|
|
94
95
|
"The organization's ingest key has been revoked. Contact a Workspace administrator" (Workspace key) or
|
|
95
96
|
"Your ingest key has been revoked. Run `harnessforce init`" (user key)
|
|
96
97
|
|
|
98
|
+
## Work location hooks
|
|
99
|
+
|
|
100
|
+
A session often starts in the repository's main checkout and then works in a worktree or on a new
|
|
101
|
+
branch. To link such runs to their pull requests, the plugin records the repository and branch of
|
|
102
|
+
each Git working tree the session works in, at the time it observes it: the new working directory on
|
|
103
|
+
`CwdChanged`, the directory of the file on `PostToolUse` of `Write` and `Edit`, and `cwd` on `Stop`.
|
|
104
|
+
At the end of each turn (`Stop`), when these pairs differ from the pairs sent last, it sends them to
|
|
105
|
+
`POST <HARNESSFORCE_ENDPOINT>/v1/sessions/locations`.
|
|
106
|
+
|
|
107
|
+
- It sends only the agent, the session ID and up to 10 pairs of repository and branch. Paths,
|
|
108
|
+
commits, file names and the content of edits are never sent. The record lives in
|
|
109
|
+
`scratchpad_dir` (`locations-<session_id>.json`); without `scratchpad_dir` nothing is recorded.
|
|
110
|
+
- The hooks run asynchronously and never delay a turn. A failure writes
|
|
111
|
+
`harnessforce: session location failed (<reason>)` to stderr and is retried at the next turn; after a
|
|
112
|
+
`401` the session stops sending. `CwdChanged` writes nothing to stderr.
|
|
113
|
+
|
|
97
114
|
## Config snapshot
|
|
98
115
|
|
|
99
116
|
At the same `SessionStart`, the hook also sends a config snapshot to
|
package/commands/setup.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Harnessforce setup. Walks through checking Node.js, choosing whether to send
|
|
2
|
+
description: Harnessforce setup. Walks through checking Node.js, choosing whether to send session content, installing the CLI, running harnessforce init, importing past sessions, and confirming the first event after the restart
|
|
3
3
|
disable-model-invocation: true
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -25,28 +25,26 @@ Run the following, then confirm the install with `harnessforce --version`.
|
|
|
25
25
|
npm install -g @harnessforce/cli
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
## 4. Choose whether to send
|
|
28
|
+
## 4. Choose whether to send content
|
|
29
29
|
|
|
30
|
-
Before `harnessforce init`, present the option to send
|
|
30
|
+
Before `harnessforce init`, present the option to send the content of the user's sessions, with "send" selected by default. The login is not done yet at this point, so neither the Workspace to connect nor what it stores is known. Do not change what you show depending on the Workspace; `harnessforce init` decides each category from the Workspace it connects, and writes `0` for a category the Workspace does not store.
|
|
31
31
|
|
|
32
32
|
Present the option together with the following.
|
|
33
33
|
|
|
34
|
-
- If selected, the
|
|
35
|
-
- Content is stored only when both this setting on the device and the Workspace
|
|
36
|
-
- If selected, run `harnessforce init --send-content`; if not, run `harnessforce init` (step 5). Write this setting to the `env` of the Claude Code user settings, and do not write it to the `env` of the project (`.claude/settings.json`) or local (`.claude/settings.local.json`) files, because the content
|
|
37
|
-
- To stop
|
|
34
|
+
- If selected, the prompts the user types, model responses, and tool input and output (including Bash commands and file paths) are delivered to this Workspace for the categories the Workspace stores. Secret patterns, email addresses, and phone numbers are replaced with `[REDACTED:kind]` before storing, and the content is deleted after the content retention period (30 days by default, at most 30 days). Secrets and personal information that do not match a pattern are not masked, and not everything can be detected. Who can read the stored content follows the Workspace setting (everyone in the Workspace by default); `harnessforce init` shows the setting of the Workspace it connects. The user can delete their own content from the account settings, and the sender, owners and admins can delete a session's content at any time.
|
|
35
|
+
- Content is stored only when both this setting on the device and the Workspace's storing of that category are in place. If the Workspace stops storing a category, later content is discarded on receipt.
|
|
36
|
+
- If selected, run `harnessforce init --send-content`; if not, run `harnessforce init` (step 5). Write this setting to the `env` of the Claude Code user settings, and do not write it to the `env` of the project (`.claude/settings.json`) or local (`.claude/settings.local.json`) files, because the content variables are ignored in the `env` of those two files. A restart is needed after `harnessforce init` for the setting to take effect.
|
|
37
|
+
- To stop afterwards, run `harnessforce init` again without `--send-content`. It rewrites the content variables to `0`.
|
|
38
38
|
|
|
39
39
|
Use the following option text as it is.
|
|
40
40
|
|
|
41
|
-
>
|
|
42
|
-
>
|
|
43
|
-
> Content is not sent (by default). You can enable it later.
|
|
41
|
+
> Send the content of your sessions (on by default). The prompts you type, model responses and tool input and output (including Bash commands and file paths) are delivered to this workspace and stored for the categories the workspace stores. Values that match known patterns for secrets and personal information are replaced with `[REDACTED:kind]`, but not everything can be detected. Content is deleted after the content retention period (30 days by default, at most 30 days). Who can read it follows the workspace setting (everyone in the workspace by default); `harnessforce init` shows the setting of the workspace you connect. You can delete your own content from your account settings, and the sender, owners and admins can delete a session's content at any time.
|
|
44
42
|
|
|
45
43
|
## 5. Connect to Harnessforce
|
|
46
44
|
|
|
47
45
|
Follow the choice in step 4: run `harnessforce init --send-content` if selected, or `harnessforce init` if not. Tell the user to log in to Harnessforce in the browser and choose one Workspace to connect. If the browser does not open, show the user the URL that `harnessforce init` prints. The command waits up to 5 minutes for the login to complete, so run it with a Bash timeout of 10 minutes.
|
|
48
46
|
|
|
49
|
-
For a Workspace that
|
|
47
|
+
For a Workspace that stores no content, `harnessforce init --send-content` writes the content variables as `0`. It prints a message about that and about Collection & retention in the Workspace settings, and exits with code 0. Do not treat this as a failure; show the printed text to the user as it is.
|
|
50
48
|
|
|
51
49
|
## 6. Import past sessions
|
|
52
50
|
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Register Claude Code sessions with Harnessforce and send their usage summaries",
|
|
2
|
+
"description": "Register Claude Code sessions with Harnessforce and send their usage summaries and work locations",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -60,6 +60,20 @@
|
|
|
60
60
|
"async": true
|
|
61
61
|
}
|
|
62
62
|
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"matcher": "^(Write|Edit)$",
|
|
66
|
+
"hooks": [
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "node",
|
|
70
|
+
"args": [
|
|
71
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/harnessforce-hook.cjs",
|
|
72
|
+
"work-location"
|
|
73
|
+
],
|
|
74
|
+
"async": true
|
|
75
|
+
}
|
|
76
|
+
]
|
|
63
77
|
}
|
|
64
78
|
],
|
|
65
79
|
"PostToolUseFailure": [
|
|
@@ -122,6 +136,36 @@
|
|
|
122
136
|
}
|
|
123
137
|
]
|
|
124
138
|
}
|
|
139
|
+
],
|
|
140
|
+
"CwdChanged": [
|
|
141
|
+
{
|
|
142
|
+
"hooks": [
|
|
143
|
+
{
|
|
144
|
+
"type": "command",
|
|
145
|
+
"command": "node",
|
|
146
|
+
"args": [
|
|
147
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/harnessforce-hook.cjs",
|
|
148
|
+
"cwd-changed"
|
|
149
|
+
],
|
|
150
|
+
"async": true
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
"Stop": [
|
|
156
|
+
{
|
|
157
|
+
"hooks": [
|
|
158
|
+
{
|
|
159
|
+
"type": "command",
|
|
160
|
+
"command": "node",
|
|
161
|
+
"args": [
|
|
162
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/harnessforce-hook.cjs",
|
|
163
|
+
"stop"
|
|
164
|
+
],
|
|
165
|
+
"async": true
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
125
169
|
]
|
|
126
170
|
}
|
|
127
171
|
}
|
package/package.json
CHANGED
|
@@ -28,8 +28,7 @@ function isInstant(value) {
|
|
|
28
28
|
const nanos = Number(fraction.slice(0, 9) || "0");
|
|
29
29
|
return secondMs >= MIN_INSTANT_MS && !(secondMs > MAX_INSTANT_MS || secondMs === MAX_INSTANT_MS && nanos > 0);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
//#region ../../packages/semconv/dist/repository.js
|
|
31
|
+
// ../../packages/semconv/dist/repository.js
|
|
33
32
|
const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
34
33
|
const SCP_LIKE = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/;
|
|
35
34
|
const HOST = /^[a-z0-9.-]+$/;
|
|
@@ -63,11 +62,9 @@ function normalizeRepository(remoteUrl) {
|
|
|
63
62
|
const [owner, name] = host === "github.com" ? segments.map((segment) => segment.toLowerCase()) : segments;
|
|
64
63
|
return `${host}/${owner}/${name}`;
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
//#region ../../packages/agent-core/src/object.ts
|
|
65
|
+
// ../../packages/agent-core/src/object.ts
|
|
68
66
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
-
|
|
70
|
-
//#region ../../packages/agent-core/src/config/canonical.ts
|
|
67
|
+
// ../../packages/agent-core/src/config/canonical.ts
|
|
71
68
|
const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
72
69
|
function canonicalJson(value) {
|
|
73
70
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
@@ -83,8 +80,7 @@ function hashFileContent(content) {
|
|
|
83
80
|
function sortComponents(components) {
|
|
84
81
|
return [...components].sort((a, b) => byCodeUnit$1(a.kind, b.kind) || byCodeUnit$1(a.source, b.source) || byCodeUnit$1(a.id, b.id));
|
|
85
82
|
}
|
|
86
|
-
|
|
87
|
-
//#region ../../packages/agent-core/src/config/files.ts
|
|
83
|
+
// ../../packages/agent-core/src/config/files.ts
|
|
88
84
|
const MAX_DEPTH = 8;
|
|
89
85
|
const MAX_FILE_BYTES = 1048576;
|
|
90
86
|
var CollectionExpired = class extends Error {};
|
|
@@ -145,8 +141,7 @@ async function listDirectories(dir, guard) {
|
|
|
145
141
|
for (const entry of await entries(dir, guard)) if (await kindOf(dir, entry, guard) === "directory") names.push(entry.name);
|
|
146
142
|
return names;
|
|
147
143
|
}
|
|
148
|
-
|
|
149
|
-
//#region ../../packages/agent-core/src/config/hash-input.ts
|
|
144
|
+
// ../../packages/agent-core/src/config/hash-input.ts
|
|
150
145
|
function withoutSecrets(entry) {
|
|
151
146
|
const { env, headers, url, args: _args, ...rest } = entry;
|
|
152
147
|
const result = rest;
|
|
@@ -178,8 +173,7 @@ function hashInput(kind, value) {
|
|
|
178
173
|
if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
|
|
179
174
|
return value;
|
|
180
175
|
}
|
|
181
|
-
|
|
182
|
-
//#region ../../packages/agent-core/src/config/scope.ts
|
|
176
|
+
// ../../packages/agent-core/src/config/scope.ts
|
|
183
177
|
const MAX_COMPONENTS = 1e3;
|
|
184
178
|
var TooManyComponents = class extends Error {};
|
|
185
179
|
var Sink = class {
|
|
@@ -235,8 +229,7 @@ function collectMcpServers(s, servers) {
|
|
|
235
229
|
if (!isObject(servers)) return;
|
|
236
230
|
for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
|
|
237
231
|
}
|
|
238
|
-
|
|
239
|
-
//#region ../../packages/agent-core/src/hook-warning.ts
|
|
232
|
+
// ../../packages/agent-core/src/hook-warning.ts
|
|
240
233
|
const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
|
|
241
234
|
const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
|
|
242
235
|
const REGISTRATION_FAILED_WARNING = "Harnessforce session registration failed. Check your network connection";
|
|
@@ -250,8 +243,7 @@ const DESTINATION_MISMATCH_WARNING = {
|
|
|
250
243
|
};
|
|
251
244
|
const TELEMETRY_NOT_CONFIGURED_WARNING = "Claude Code telemetry for Harnessforce is not configured. Run `harnessforce init`";
|
|
252
245
|
const warningOnlyOutput = (message) => `${JSON.stringify({ systemMessage: message })}\n`;
|
|
253
|
-
|
|
254
|
-
//#region ../../packages/agent-core/src/managed.ts
|
|
246
|
+
// ../../packages/agent-core/src/managed.ts
|
|
255
247
|
const MANAGED_DIRS = {
|
|
256
248
|
darwin: "/Library/Application Support/ClaudeCode",
|
|
257
249
|
win32: "C:\\Program Files\\ClaudeCode"
|
|
@@ -270,8 +262,7 @@ async function readManagedEnv(managedDir, names) {
|
|
|
270
262
|
}
|
|
271
263
|
return values;
|
|
272
264
|
}
|
|
273
|
-
|
|
274
|
-
//#region ../../packages/agent-core/src/plugin-auto-update.ts
|
|
265
|
+
// ../../packages/agent-core/src/plugin-auto-update.ts
|
|
275
266
|
const MARKETPLACE_NAME = "harnessforce-agent";
|
|
276
267
|
const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
|
|
277
268
|
const DISABLING_VARIABLES = [
|
|
@@ -314,8 +305,7 @@ async function isPluginAutoUpdateEffective(options) {
|
|
|
314
305
|
const entry = marketplacesOf(await readJsonObjectFile(options.userSettingsPath, noGuard$1))?.[MARKETPLACE_NAME];
|
|
315
306
|
return isObject(entry) && entry.autoUpdate === true;
|
|
316
307
|
}
|
|
317
|
-
|
|
318
|
-
//#region ../../packages/agent-core/src/process/lookup.ts
|
|
308
|
+
// ../../packages/agent-core/src/process/lookup.ts
|
|
319
309
|
const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
320
310
|
const NPM_SHIM_MAX_BYTES = 65536;
|
|
321
311
|
const SHEBANG_MAX_BYTES = 256;
|
|
@@ -496,8 +486,7 @@ async function commandLineFor(file, args, context, { quoteArgs = true } = {}) {
|
|
|
496
486
|
verbatim: true
|
|
497
487
|
};
|
|
498
488
|
}
|
|
499
|
-
|
|
500
|
-
//#region ../../packages/agent-core/src/process/git.ts
|
|
489
|
+
// ../../packages/agent-core/src/process/git.ts
|
|
501
490
|
const GIT_TIMEOUT_MS = 1e3;
|
|
502
491
|
const execGit = (file, args, env, timeoutMs) => new Promise((resolve) => {
|
|
503
492
|
(0, node_child_process.execFile)(file, args, {
|
|
@@ -535,8 +524,7 @@ function createGitRunner({ platform, env, processCwd, excludeTarget, timeoutMs =
|
|
|
535
524
|
], gitEnv, timeoutMs);
|
|
536
525
|
};
|
|
537
526
|
}
|
|
538
|
-
|
|
539
|
-
//#region ../../packages/agent-core/src/process/runtime-env.ts
|
|
527
|
+
// ../../packages/agent-core/src/process/runtime-env.ts
|
|
540
528
|
const RUNTIME_VARIABLES = [
|
|
541
529
|
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
542
530
|
"NODE_EXTRA_CA_CERTS",
|
|
@@ -600,8 +588,7 @@ function relaunchWithoutRuntimeVariables({ platform, env, stash, spawnSelf: star
|
|
|
600
588
|
}));
|
|
601
589
|
});
|
|
602
590
|
}
|
|
603
|
-
|
|
604
|
-
//#region ../../packages/agent-core/src/telemetry-settings.ts
|
|
591
|
+
// ../../packages/agent-core/src/telemetry-settings.ts
|
|
605
592
|
const EXPECTED = {
|
|
606
593
|
CLAUDE_CODE_ENABLE_TELEMETRY: (value) => value === "1",
|
|
607
594
|
OTEL_LOGS_EXPORTER: (value) => includesOtlp(value),
|
|
@@ -621,8 +608,7 @@ async function isTelemetryConfigured(options) {
|
|
|
621
608
|
return typeof value === "string" && EXPECTED[name](value);
|
|
622
609
|
}) && typeof settings?.otelHeadersHelper === "string";
|
|
623
610
|
}
|
|
624
|
-
|
|
625
|
-
//#region ../../packages/agent-core/src/url.ts
|
|
611
|
+
// ../../packages/agent-core/src/url.ts
|
|
626
612
|
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
627
613
|
"localhost",
|
|
628
614
|
"127.0.0.1",
|
|
@@ -643,8 +629,7 @@ function underBase(base, path) {
|
|
|
643
629
|
url.pathname = `${base.pathname.replace(/\/+$/, "")}/${path}`;
|
|
644
630
|
return url;
|
|
645
631
|
}
|
|
646
|
-
|
|
647
|
-
//#region ../../packages/agent-core/src/client-update.ts
|
|
632
|
+
// ../../packages/agent-core/src/client-update.ts
|
|
648
633
|
const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
|
|
649
634
|
const UPDATE_COMMANDS = {
|
|
650
635
|
cli: "npm install -g @harnessforce/cli@latest",
|
|
@@ -803,8 +788,7 @@ async function recheckClientStatus(options) {
|
|
|
803
788
|
}
|
|
804
789
|
return await readUpdateState(options.statePath, options.version) ?? state;
|
|
805
790
|
}
|
|
806
|
-
|
|
807
|
-
//#region ../../packages/agent-core/src/config/plugins.ts
|
|
791
|
+
// ../../packages/agent-core/src/config/plugins.ts
|
|
808
792
|
const NON_MARKETPLACE_ORIGINS = /* @__PURE__ */ new Set([
|
|
809
793
|
"inline",
|
|
810
794
|
"skills-dir",
|
|
@@ -845,8 +829,7 @@ async function collectPlugins(pluginsRoot, settingsByPrecedence, scope, guard) {
|
|
|
845
829
|
collectMcpServers(s, (await readJsonObjectFile((0, node_path.join)(root, ".mcp.json"), guard))?.mcpServers);
|
|
846
830
|
}
|
|
847
831
|
}
|
|
848
|
-
|
|
849
|
-
//#region ../../packages/agent-core/src/config/collect.ts
|
|
832
|
+
// ../../packages/agent-core/src/config/collect.ts
|
|
850
833
|
const COLLECT_BUDGET_MS = 1e3;
|
|
851
834
|
async function readManagedSettings(managedDir, guard) {
|
|
852
835
|
let merged = await readJsonObjectFile((0, node_path.join)(managedDir, "managed-settings.json"), guard);
|
|
@@ -980,8 +963,7 @@ async function collectConfig(options) {
|
|
|
980
963
|
components: sortComponents(sink.components)
|
|
981
964
|
};
|
|
982
965
|
}
|
|
983
|
-
|
|
984
|
-
//#region ../../packages/agent-core/src/ingest.ts
|
|
966
|
+
// ../../packages/agent-core/src/ingest.ts
|
|
985
967
|
const SEND_TIMEOUT_MS = 2e3;
|
|
986
968
|
const INGEST_KEY_WORKSPACE = /^hf_ik_([A-Za-z0-9-]{1,128})_/;
|
|
987
969
|
const workspaceIdOf = (key) => INGEST_KEY_WORKSPACE.exec(key)?.[1];
|
|
@@ -992,6 +974,7 @@ function ingestBaseFrom(endpoint) {
|
|
|
992
974
|
ingestBase.pathname = base.pathname.replace(/\/+$/, "");
|
|
993
975
|
return ingestBase;
|
|
994
976
|
}
|
|
977
|
+
const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) => postItems(destination, path, [item], fetchImpl, timeoutMs);
|
|
995
978
|
async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) {
|
|
996
979
|
try {
|
|
997
980
|
const response = await fetchImpl(underBase(destination.ingestBase, path), {
|
|
@@ -1017,21 +1000,17 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
|
|
|
1017
1000
|
};
|
|
1018
1001
|
}
|
|
1019
1002
|
}
|
|
1020
|
-
|
|
1021
|
-
//#region ../../packages/agent-core/src/issue.ts
|
|
1003
|
+
// ../../packages/agent-core/src/issue.ts
|
|
1022
1004
|
const isIssueIdentifier = (value) => isToken(value, 300);
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
//#endregion
|
|
1027
|
-
//#region ../../packages/agent-core/src/session-context.ts
|
|
1005
|
+
// ../../packages/agent-core/src/using-harnessforce.ts
|
|
1006
|
+
const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Work locations: the repository and branch of each Git working tree the session works in. Paths are not sent.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user sends them (`--send-content` or managed settings) and the Workspace stores that category. The members the Workspace chooses can read them. `harnessforce import` never sends them.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- Stop: sends the work locations at the end of a turn in which they changed.\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
|
|
1007
|
+
// ../../packages/agent-core/src/session-context.ts
|
|
1028
1008
|
const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
|
|
1029
1009
|
const sessionContext = (sessionId) => ({ hookSpecificOutput: {
|
|
1030
1010
|
hookEventName: "SessionStart",
|
|
1031
1011
|
additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
|
|
1032
1012
|
} });
|
|
1033
|
-
|
|
1034
|
-
//#region ../../packages/agent-core/src/vcs.ts
|
|
1013
|
+
// ../../packages/agent-core/src/vcs.ts
|
|
1035
1014
|
async function selectRemote(cwd, git) {
|
|
1036
1015
|
if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
|
|
1037
1016
|
const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
|
|
@@ -1070,8 +1049,120 @@ async function resolveProjectRoot(cwd, git) {
|
|
|
1070
1049
|
const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1071
1050
|
return topLevel && (0, node_path.isAbsolute)(topLevel) ? topLevel : cwd;
|
|
1072
1051
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1052
|
+
// ../../packages/agent-core/src/work-location.ts
|
|
1053
|
+
const absolute = (value) => typeof value === "string" && (0, node_path.isAbsolute)(value) ? value : void 0;
|
|
1054
|
+
const toolInputOf = (fields) => isObject(fields.tool_input) ? fields.tool_input : {};
|
|
1055
|
+
const PATCH_FILE_LINE = /^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/;
|
|
1056
|
+
function patchedDirectories(command, cwd) {
|
|
1057
|
+
if (typeof command !== "string") return [];
|
|
1058
|
+
return command.split(/\r?\n/).flatMap((line) => {
|
|
1059
|
+
const path = PATCH_FILE_LINE.exec(line)?.[1]?.trim();
|
|
1060
|
+
return path ? [(0, node_path.dirname)((0, node_path.resolve)(cwd, path))] : [];
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* eventの入力から、作業位置を求めるdirectoryを返す。読むのは`new_cwd`、`file_path`、patchのfileの行のpathだけで、編集の
|
|
1065
|
+
* 本文を読まない。
|
|
1066
|
+
*/
|
|
1067
|
+
function observedDirectories(agent, event, fields, cwd) {
|
|
1068
|
+
if (event === "stop") return [cwd];
|
|
1069
|
+
if (agent === "claude_code" && event === "cwd-changed") {
|
|
1070
|
+
const newCwd = absolute(fields.new_cwd);
|
|
1071
|
+
return newCwd ? [newCwd] : [];
|
|
1072
|
+
}
|
|
1073
|
+
if (event !== "post-tool-use") return [];
|
|
1074
|
+
const input = toolInputOf(fields);
|
|
1075
|
+
if (agent === "codex") return fields.tool_name === "apply_patch" ? [...new Set(patchedDirectories(input.command, cwd))] : [];
|
|
1076
|
+
if (fields.tool_name !== "Write" && fields.tool_name !== "Edit") return [];
|
|
1077
|
+
const filePath = absolute(input.file_path);
|
|
1078
|
+
return filePath ? [(0, node_path.dirname)(filePath)] : [];
|
|
1079
|
+
}
|
|
1080
|
+
const isDirectory = (path) => (0, node_fs_promises.stat)(path).then((found) => found.isDirectory(), () => false);
|
|
1081
|
+
/** directoryのworking treeの、その時点のrepositoryとbranch。repositoryかbranchを求められなければundefined。 */
|
|
1082
|
+
async function observePair(directory, git) {
|
|
1083
|
+
if (!await isDirectory(directory)) return void 0;
|
|
1084
|
+
const vcs = await resolveVcs(directory, git);
|
|
1085
|
+
return vcs?.repository && vcs.branch ? {
|
|
1086
|
+
repository: vcs.repository,
|
|
1087
|
+
branch: vcs.branch
|
|
1088
|
+
} : void 0;
|
|
1089
|
+
}
|
|
1090
|
+
const emptyLocationState = () => ({
|
|
1091
|
+
pairs: [],
|
|
1092
|
+
sent: [],
|
|
1093
|
+
unauthorized: false
|
|
1094
|
+
});
|
|
1095
|
+
const pairKey = (pair) => JSON.stringify([pair.repository, pair.branch]);
|
|
1096
|
+
/** 組を加える。記録済みの組と、10件に達した後の組は加えない。 */
|
|
1097
|
+
function withObservedPair(state, pair) {
|
|
1098
|
+
if (state.pairs.some((saved) => pairKey(saved) === pairKey(pair)) || state.pairs.length >= 10) return state;
|
|
1099
|
+
return {
|
|
1100
|
+
...state,
|
|
1101
|
+
pairs: [...state.pairs, pair]
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
/** 記録した組の集合が、最後に送った集合と異なる場合だけ送る組。 */
|
|
1105
|
+
function locationsToSend(state) {
|
|
1106
|
+
if (state.unauthorized || state.pairs.length === 0) return void 0;
|
|
1107
|
+
const sent = new Set(state.sent.map(pairKey));
|
|
1108
|
+
return sent.size === state.pairs.length && state.pairs.every((pair) => sent.has(pairKey(pair))) ? void 0 : state.pairs;
|
|
1109
|
+
}
|
|
1110
|
+
const isPair = (value) => isObject(value) && typeof value.repository === "string" && typeof value.branch === "string";
|
|
1111
|
+
async function readLocationState(path) {
|
|
1112
|
+
try {
|
|
1113
|
+
const saved = JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8"));
|
|
1114
|
+
if (!isObject(saved) || !Array.isArray(saved.pairs) || !Array.isArray(saved.sent)) return emptyLocationState();
|
|
1115
|
+
return {
|
|
1116
|
+
pairs: saved.pairs.filter(isPair),
|
|
1117
|
+
sent: saved.sent.filter(isPair),
|
|
1118
|
+
unauthorized: saved.unauthorized === true
|
|
1119
|
+
};
|
|
1120
|
+
} catch {
|
|
1121
|
+
return emptyLocationState();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function writeLocationState(path, state) {
|
|
1125
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1126
|
+
await (0, node_fs_promises.writeFile)(temporary, JSON.stringify(state), { mode: 384 });
|
|
1127
|
+
await (0, node_fs_promises.rename)(temporary, path);
|
|
1128
|
+
}
|
|
1129
|
+
const FILE_SAFE_SESSION_ID$1 = /^[A-Za-z0-9_-]+$/;
|
|
1130
|
+
/** Claude Codeの記録のfile。`scratchpad_dir`が無ければ記録も送信もしない。 */
|
|
1131
|
+
const claudeLocationStatePath = (scratchpadDir, sessionId) => FILE_SAFE_SESSION_ID$1.test(sessionId) ? (0, node_path.join)(scratchpadDir, `locations-${sessionId}.json`) : void 0;
|
|
1132
|
+
/** 観測したdirectoryの組を記録のfileへ加える。gitの外のdirectoryは記録しない。 */
|
|
1133
|
+
async function recordObservedPairs(statePath, directories, deps) {
|
|
1134
|
+
const pairs = await Promise.all(directories.map((directory) => observePair(directory, deps.git)));
|
|
1135
|
+
const before = await readLocationState(statePath);
|
|
1136
|
+
const after = pairs.reduce((state, pair) => pair ? withObservedPair(state, pair) : state, before);
|
|
1137
|
+
if (after !== before) await writeLocationState(statePath, after);
|
|
1138
|
+
return after;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* 記録した組の集合が最後に送った集合と異なれば`/v1/sessions/locations`へ送る。2xxなら送った集合を記録し、401なら以後送らない
|
|
1142
|
+
* ことを記録する。それ以外の失敗では記録を変えず、次の`Stop`が送り直しを兼ねる。
|
|
1143
|
+
*/
|
|
1144
|
+
async function sendLocations(statePath, state, item, destination, fetchImpl) {
|
|
1145
|
+
const locations = locationsToSend(state);
|
|
1146
|
+
if (!locations) return { kind: "unchanged" };
|
|
1147
|
+
const outcome = await postItem(destination, "v1/sessions/locations", {
|
|
1148
|
+
agent: item.agent,
|
|
1149
|
+
session_id: item.sessionId,
|
|
1150
|
+
locations
|
|
1151
|
+
}, fetchImpl);
|
|
1152
|
+
if (outcome.kind === "accepted") await writeLocationState(statePath, {
|
|
1153
|
+
...state,
|
|
1154
|
+
sent: locations
|
|
1155
|
+
});
|
|
1156
|
+
if (outcome.kind === "unauthorized") await writeLocationState(statePath, {
|
|
1157
|
+
...state,
|
|
1158
|
+
unauthorized: true
|
|
1159
|
+
});
|
|
1160
|
+
return {
|
|
1161
|
+
kind: "sent",
|
|
1162
|
+
outcome
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
// src/input.ts
|
|
1075
1166
|
const FILE_SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
1076
1167
|
const isFileSafeSessionId = (sessionId) => FILE_SAFE_SESSION_ID.test(sessionId);
|
|
1077
1168
|
const token = (value) => typeof value === "string" && isToken(value) ? value : void 0;
|
|
@@ -1102,8 +1193,7 @@ function parseHookInput(raw) {
|
|
|
1102
1193
|
fields
|
|
1103
1194
|
};
|
|
1104
1195
|
}
|
|
1105
|
-
|
|
1106
|
-
//#region src/scratchpad.ts
|
|
1196
|
+
// src/scratchpad.ts
|
|
1107
1197
|
const registrationFile = (pad) => (0, node_path.join)(pad.dir, `registration-${pad.sessionId}.json`);
|
|
1108
1198
|
const unauthorizedMark = (pad) => (0, node_path.join)(pad.dir, `unauthorized-${pad.sessionId}`);
|
|
1109
1199
|
const firstPromptSentMark = (pad) => (0, node_path.join)(pad.dir, `first-prompt-sent-${pad.sessionId}`);
|
|
@@ -1130,8 +1220,7 @@ async function isFirstPromptSent(pad) {
|
|
|
1130
1220
|
async function claimFirstPrompt(pad) {
|
|
1131
1221
|
return (0, node_fs_promises.writeFile)(firstPromptSentMark(pad), "", { flag: "wx" }).then(() => true, () => false);
|
|
1132
1222
|
}
|
|
1133
|
-
|
|
1134
|
-
//#region src/usage/names.ts
|
|
1223
|
+
// src/usage/names.ts
|
|
1135
1224
|
const PLUGIN_COMPONENT = /^([A-Za-z0-9._-]+)@[A-Za-z0-9._-]+:(.+)$/;
|
|
1136
1225
|
function pluginPartsOf(identifier) {
|
|
1137
1226
|
const parts = identifier.plugin ? PLUGIN_COMPONENT.exec(identifier.id) : null;
|
|
@@ -1167,8 +1256,7 @@ const mcpServerOfTool = (toolName, servers) => onlyOne(servers.filter((server) =
|
|
|
1167
1256
|
const prefix = toolPrefixOf(server);
|
|
1168
1257
|
return prefix !== void 0 && toolName.startsWith(prefix);
|
|
1169
1258
|
}));
|
|
1170
|
-
|
|
1171
|
-
//#region src/usage/count.ts
|
|
1259
|
+
// src/usage/count.ts
|
|
1172
1260
|
const COUNTING_EVENTS = [
|
|
1173
1261
|
"post-tool-use",
|
|
1174
1262
|
"post-tool-use-failure",
|
|
@@ -1218,8 +1306,7 @@ function countedOf(event, fields, identifiers) {
|
|
|
1218
1306
|
} : void 0;
|
|
1219
1307
|
}
|
|
1220
1308
|
}
|
|
1221
|
-
|
|
1222
|
-
//#region src/usage/store.ts
|
|
1309
|
+
// src/usage/store.ts
|
|
1223
1310
|
function usageStoreOf(sessionId, env) {
|
|
1224
1311
|
const data = env.CLAUDE_PLUGIN_DATA;
|
|
1225
1312
|
if (!data || !(0, node_path.isAbsolute)(data) || !isFileSafeSessionId(sessionId)) return void 0;
|
|
@@ -1338,8 +1425,7 @@ async function removeExpiredFiles(store, now) {
|
|
|
1338
1425
|
if (await (0, node_fs_promises.unlink)(file).then(() => true, () => false)) removed += 1;
|
|
1339
1426
|
}
|
|
1340
1427
|
}
|
|
1341
|
-
|
|
1342
|
-
//#region src/usage/summary.ts
|
|
1428
|
+
// src/usage/summary.ts
|
|
1343
1429
|
const COLLECTOR_VERSION = "0.1.0";
|
|
1344
1430
|
const MAX_ITEMS = 200;
|
|
1345
1431
|
const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -1416,8 +1502,7 @@ function buildSummary(sessionId, lines) {
|
|
|
1416
1502
|
}
|
|
1417
1503
|
};
|
|
1418
1504
|
}
|
|
1419
|
-
|
|
1420
|
-
//#region src/usage/send.ts
|
|
1505
|
+
// src/usage/send.ts
|
|
1421
1506
|
async function sendSessionUsage(store, destination, deadlineMs, deps) {
|
|
1422
1507
|
const state = await readState(store);
|
|
1423
1508
|
if (!state || state.workspaceId !== workspaceIdOf(destination.key)) return void 0;
|
|
@@ -1470,8 +1555,7 @@ async function sendUnsentUsage(store, destination, deps) {
|
|
|
1470
1555
|
if (outcome.kind === "accepted") await Promise.all(batch.map((sent) => writeSentLength(store, sent.sessionId, sent.state, sent.record.length)));
|
|
1471
1556
|
return outcome;
|
|
1472
1557
|
}
|
|
1473
|
-
|
|
1474
|
-
//#region src/hook.ts
|
|
1558
|
+
// src/hook.ts
|
|
1475
1559
|
const pluginStatePath = (env) => env.CLAUDE_PLUGIN_DATA && (0, node_path.isAbsolute)(env.CLAUDE_PLUGIN_DATA) ? (0, node_path.join)(env.CLAUDE_PLUGIN_DATA, "update-status.json") : void 0;
|
|
1476
1560
|
const cliStatePath = (homeDir) => (0, node_path.join)(homeDir, ".harnessforce", "update-status.json");
|
|
1477
1561
|
const CLI_UNSUPPORTED_WARNING = unsupportedLine("cli");
|
|
@@ -1750,10 +1834,43 @@ const onCountingEvent = (event) => async (input, deps) => {
|
|
|
1750
1834
|
...counted
|
|
1751
1835
|
});
|
|
1752
1836
|
};
|
|
1837
|
+
const locationStatePathOf = (input) => input.scratchpad ? claudeLocationStatePath(input.scratchpad.dir, input.sessionId) : void 0;
|
|
1838
|
+
/**
|
|
1839
|
+
* correlation.md「sessionの作業位置」: 観測した時点のworking treeのrepositoryとbranchを記録する。`Stop`は加えて、記録した組の
|
|
1840
|
+
* 集合が最後に送った集合と異なれば送る。stdoutへ何も書かず、`CwdChanged`ではstderrにも書かない。
|
|
1841
|
+
*/
|
|
1842
|
+
const onLocationEvent = (event) => async (input, deps) => {
|
|
1843
|
+
const statePath = locationStatePathOf(input);
|
|
1844
|
+
if (!statePath) return;
|
|
1845
|
+
const state = await recordObservedPairs(statePath, observedDirectories("claude_code", event, input.fields, input.cwd), deps);
|
|
1846
|
+
if (event !== "stop" || !input.scratchpad) return;
|
|
1847
|
+
if (await isMarkedUnauthorized(input.scratchpad)) return;
|
|
1848
|
+
if (await isPluginBlocked(deps, false)) return;
|
|
1849
|
+
const { destination } = await resolveDestination(deps, input.cwd);
|
|
1850
|
+
if (!destination) return;
|
|
1851
|
+
const sent = await sendLocations(statePath, state, {
|
|
1852
|
+
agent: "claude_code",
|
|
1853
|
+
sessionId: input.sessionId
|
|
1854
|
+
}, destination, deps.fetch);
|
|
1855
|
+
if (sent.kind !== "sent" || sent.outcome.kind === "accepted") return;
|
|
1856
|
+
if (sent.outcome.kind === "unauthorized") {
|
|
1857
|
+
await markUnauthorized(input.scratchpad).catch(logError(deps));
|
|
1858
|
+
deps.stderr("harnessforce: session location failed (unauthorized)\n");
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
deps.stderr(`harnessforce: session location failed (${sent.outcome.reason})\n`);
|
|
1862
|
+
};
|
|
1863
|
+
const silently = (handler) => (input, deps) => handler(input, {
|
|
1864
|
+
...deps,
|
|
1865
|
+
stderr: () => {}
|
|
1866
|
+
}).catch(() => {});
|
|
1753
1867
|
const HANDLERS = new Map([
|
|
1754
1868
|
["session-start", onSessionStart],
|
|
1755
1869
|
["user-prompt-submit", onUserPromptSubmit],
|
|
1756
1870
|
["session-end", onSessionEnd],
|
|
1871
|
+
["cwd-changed", silently(onLocationEvent("cwd-changed"))],
|
|
1872
|
+
["work-location", onLocationEvent("post-tool-use")],
|
|
1873
|
+
["stop", onLocationEvent("stop")],
|
|
1757
1874
|
...COUNTING_EVENTS.map((event) => [event, onCountingEvent(event)])
|
|
1758
1875
|
]);
|
|
1759
1876
|
async function runHook(event, rawInput, hookDeps) {
|
|
@@ -1767,8 +1884,7 @@ async function runHook(event, rawInput, hookDeps) {
|
|
|
1767
1884
|
};
|
|
1768
1885
|
await handler(input, deps).catch(logError(deps));
|
|
1769
1886
|
}
|
|
1770
|
-
|
|
1771
|
-
//#region src/user-key.ts
|
|
1887
|
+
// src/user-key.ts
|
|
1772
1888
|
const HF_TIMEOUT_MS = 1e3;
|
|
1773
1889
|
const HF_OUTPUT_LIMIT_BYTES = 65536;
|
|
1774
1890
|
const BEARER = /^Bearer (\S+)$/;
|
|
@@ -1829,8 +1945,7 @@ const execHf = (file, args, options) => new Promise((resolve) => {
|
|
|
1829
1945
|
...error && !error.killed && typeof error.code === "number" ? { exitCode: error.code } : {}
|
|
1830
1946
|
}));
|
|
1831
1947
|
});
|
|
1832
|
-
|
|
1833
|
-
//#region src/main.ts
|
|
1948
|
+
// src/main.ts
|
|
1834
1949
|
delete process.env.HARNESSFORCE_RUNTIME_ENV;
|
|
1835
1950
|
async function readStdin() {
|
|
1836
1951
|
const chunks = [];
|
|
@@ -1880,7 +1995,7 @@ async function main() {
|
|
|
1880
1995
|
}),
|
|
1881
1996
|
client: {
|
|
1882
1997
|
component: "claude-code-plugin",
|
|
1883
|
-
version: "0.
|
|
1998
|
+
version: "0.3.0",
|
|
1884
1999
|
dev: false
|
|
1885
2000
|
},
|
|
1886
2001
|
isPluginAutoUpdateEffective: (cwd) => isPluginAutoUpdateEffective({
|
|
@@ -1892,4 +2007,3 @@ async function main() {
|
|
|
1892
2007
|
});
|
|
1893
2008
|
}
|
|
1894
2009
|
main();
|
|
1895
|
-
//#endregion
|
|
@@ -10,19 +10,21 @@ This session has the Harnessforce hook installed. Harnessforce links the work of
|
|
|
10
10
|
## What is recorded
|
|
11
11
|
|
|
12
12
|
- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.
|
|
13
|
+
- Work locations: the repository and branch of each Git working tree the session works in. Paths are not sent.
|
|
13
14
|
- Config snapshot: the structure of the configuration in use (Claude Code only).
|
|
14
15
|
- OTel telemetry: what the user chooses to send.
|
|
15
16
|
- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.
|
|
16
17
|
|
|
17
18
|
## What is not collected
|
|
18
19
|
|
|
19
|
-
- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user
|
|
20
|
+
- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user sends them (`--send-content` or managed settings) and the Workspace stores that category. The members the Workspace chooses can read them. `harnessforce import` never sends them.
|
|
20
21
|
- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.
|
|
21
22
|
|
|
22
23
|
## Which hook sends what
|
|
23
24
|
|
|
24
25
|
- SessionStart: session registration and the config snapshot (Codex sends session registration only).
|
|
25
26
|
- UserPromptSubmit: resends on the first prompt (Claude Code only).
|
|
27
|
+
- Stop: sends the work locations at the end of a turn in which they changed.
|
|
26
28
|
- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.
|
|
27
29
|
|
|
28
30
|
## Which skill and tool to use, and when
|