@harnessforce/claude-code-plugin 0.2.0 → 0.3.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnessforce",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Harnessforce plugin for Claude Code",
5
5
  "author": {
6
6
  "name": "Lambda Script"
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. Prompts, responses and file
76
- contents are never sent.
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,11 +1,21 @@
1
1
  ---
2
- description: Harnessforce setup. Walks through checking Node.js, choosing whether to send prompt content, installing the CLI, running harnessforce init, importing past sessions, and confirming the first event after the restart
3
- disable-model-invocation: true
2
+ description: Harnessforce setup. Walks through checking Node.js, choosing whether to send session content, installing the CLI, running harnessforce init, importing past sessions, showing the remaining workspace steps, and confirming the first event after the restart
4
3
  ---
5
4
 
6
5
  # Harnessforce setup
7
6
 
8
- Go through the steps below in order, interacting with the user. If a command fails, show its output to the user and stop without moving on to the later steps.
7
+ Go through the steps below in order, interacting with the user. Run this setup when the user types `/harnessforce:setup`, or when the user asks to install, connect or set up Harnessforce. Do not run it when the user has not asked for it. Running it again is safe: every step converges to the same state.
8
+
9
+ ## How setup ends
10
+
11
+ Setup ends only at one of the endings below, and the last line of the ending is the text shown here, as it is. Do not tell the user that setup has ended at any other point, and do not use words that say setup is finished ("complete", "done" and so on) except in the second ending of part 2.
12
+
13
+ - Node.js is missing or below 18 (step 1): the text in step 1.
14
+ - Stopped part way: "Harnessforce setup stopped at step {n} ({name}). Fix the cause shown above, then run `/harnessforce:setup` again. Steps already done are safe to repeat." Replace `{n}` and `{name}` with the number and the heading of the step that failed.
15
+ - End of part 1 (step 8): "Harnessforce setup, part 1 of 2, is done. Restart Claude Code, then run `/harnessforce:setup` again in the new session. Part 2 checks that the first event arrived."
16
+ - End of part 2 (step 9): "Harnessforce setup is complete."
17
+
18
+ When a command fails, show its output to the user. Whether setup then stops or goes on is written in each step. Errors that other plugins' hooks show do not affect setup: do not stop because of them and do not mention them.
9
19
 
10
20
  ## 1. Check Node.js
11
21
 
@@ -15,51 +25,73 @@ First run `node --version`. If the command is not found, or the version is below
15
25
 
16
26
  ## 2. Check whether this run is after the restart
17
27
 
18
- If `harnessforce --version` succeeds and the environment variable `HARNESSFORCE_WORKSPACE_ID` of this session is not empty, this is a session restarted after `harnessforce init`. Skip steps 3 to 7 and go to step 8.
28
+ If `harnessforce --version` succeeds and the environment variable `HARNESSFORCE_WORKSPACE_ID` of this session is not empty, this is a session restarted after `harnessforce init`. Skip steps 3 to 8 and go to step 9.
19
29
 
20
30
  ## 3. Install the CLI
21
31
 
22
- Run the following, then confirm the install with `harnessforce --version`.
32
+ If `harnessforce --version` succeeds, do not install anything. Otherwise run the following, then confirm the install with `harnessforce --version`.
23
33
 
24
34
  ```sh
25
35
  npm install -g @harnessforce/cli
26
36
  ```
27
37
 
28
- ## 4. Choose whether to send prompt content
38
+ If the install fails, show its output and stop at this step. When npm cannot write to its global install location (`EACCES`), also show the following before the last line.
29
39
 
30
- Before `harnessforce init`, present the option to send prompt content to the user, not selected by default. The login is not done yet at this point, so neither the Workspace to connect nor its opt-in state is known. Do not change what you show depending on the opt-in.
40
+ > npm cannot write to its global install location. Use a Node.js installed with a version manager such as nvm, or change npm's prefix to your own directory, then try again.
41
+
42
+ ## 4. Choose whether to send content
43
+
44
+ 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
45
 
32
46
  Present the option together with the following.
33
47
 
34
- - If selected, the body of the prompts the user types to the agent is delivered to this Workspace. Secret patterns, email addresses, and phone numbers are replaced with `[REDACTED:kind]` before storing, and the content is deleted after the content retention period (at most 30 days). Secrets and personal information that do not match a pattern are not masked, and not everything can be detected. Within the Workspace, only the person who sent the stored content can read it; Owners and Admins cannot read it either. That person can delete their own content at any time from My practices.
35
- - Content is stored only when both this setting on the device and the Workspace opt-in are in place. If the Workspace opt-in is withdrawn, later content is discarded on receipt and existing content data can no longer be read. This setting does not enable sending model responses or tool input and output.
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 variable is ignored in the `env` of those two files.
37
- - To stop after selecting it, set `OTEL_LOG_USER_PROMPTS` in the `env` of the user settings to `0`. The value that stops it also takes effect in the project and local `env`.
48
+ - 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.
49
+ - 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.
50
+ - 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.
51
+ - To stop afterwards, run `harnessforce init` again without `--send-content`. It rewrites the content variables to `0`.
38
52
 
39
53
  Use the following option text as it is.
40
54
 
41
- > You can send the prompts you type for analysis (off by default; it takes effect only if the workspace has opted in to prompts you typed). If enabled, the prompts you type to the agent are delivered to this workspace and stored. 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 (at most 30 days), and within the workspace only you can read it. You can delete it at any time from My practices. Enable this?
42
- >
43
- > Content is not sent (by default). You can enable it later.
55
+ > 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
56
 
45
57
  ## 5. Connect to Harnessforce
46
58
 
47
- 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.
59
+ 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, or sign up, and choose one Workspace to connect. A user without a Workspace can create one on the same page and continue. 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
60
 
49
- For a Workspace that has not opted in, `harnessforce init --send-content` does not write the content setting. It prints a message about that and about the Data retention page of 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.
61
+ 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.
62
+
63
+ If `harnessforce init` ends with the line saying the CLI version is unsupported and exit code 1, run `npm install -g @harnessforce/cli@latest`, then run this step again. If `harnessforce init` fails for any other reason, show what it printed and stop at this step; a failed `harnessforce init` changes neither the keychain nor the settings files.
50
64
 
51
65
  ## 6. Import past sessions
52
66
 
53
67
  Run `harnessforce import` and tell the user, as it is, the imported count it prints, along with the breakdown of sessions that were not imported and the guidance. Do not omit the printed reason and the next action even when the count is 0.
54
68
 
55
- ## 7. Ask for a restart
69
+ Then run `harnessforce status --json`. If `repository.connected` is `false` (the repository of the working directory is not connected), tell the user the following.
70
+
71
+ > This repository is not connected to the GitHub App, so its sessions were not imported. After connecting it, restart Claude Code and run `/harnessforce:setup` to import them.
56
72
 
57
- Tell the user the following and stop.
73
+ If `harnessforce import` fails, show its output and go on; importing is not needed for telemetry and can be run later. Before the last line of the ending, show "Importing past sessions failed. Run `harnessforce import` later." If `harnessforce status --json` fails, show its output and go on.
74
+
75
+ ## 7. Show the remaining steps
76
+
77
+ From the result of `harnessforce status --json` in step 6, show the steps that are not done (`status` other than `done` in `steps`, and every step in `admin_steps`) in the order of their numbers, each with its title, who does it, and the URL of its page. Use these titles: step 2 "Connect the GitHub App", step 3 "Assign repositories to a project", step 4 "Set up telemetry", step 5 "Wait for your first run". Steps 2 and 3 are done by an owner or admin, and steps 4 and 5 by the user. For a step the user is not responsible for (steps 2 and 3 when the user's `role` is `member`), add "Send this URL to the person responsible."
78
+
79
+ If `has_seat` is `false`, first show "You don't have a developer seat, so the telemetry you send is not stored." If no step remains, show only "Your workspace is ready." If `harnessforce status --json` failed in step 6, show the URL of the Harnessforce Home (the environment variable `HARNESSFORCE_URL`) instead of the steps.
80
+
81
+ ## 8. Ask for a restart
82
+
83
+ If the user also uses Codex, tell them first: restart Codex, then run `/hooks` in Codex and trust the harnessforce `SessionStart`, `PostToolUse`, and `Stop` hooks. Codex runs a hook only after the user trusts it, and `harnessforce init` cannot do this for them.
84
+
85
+ Then end with the following.
58
86
 
59
87
  > Harnessforce setup, part 1 of 2, is done. Restart Claude Code, then run `/harnessforce:setup` again in the new session. Part 2 checks that the first event arrived.
60
88
 
61
- ## 8. Confirm the first event
89
+ ## 9. Confirm the first event
90
+
91
+ At the start of this session, the plugin hook sent the session to Harnessforce. Run `harnessforce import` again to import the sessions of repositories connected to the GitHub App after step 6; sessions already imported are not sent twice. Tell the user the result as in step 6.
92
+
93
+ Then run `harnessforce status --json`. If step 5 is `done`, show "Your first run arrived." with the URL of the Harnessforce Home (the environment variable `HARNESSFORCE_URL`). If not, show the Home URL and tell the user to check on the waiting screen for the first run that it was received; if nothing arrives after 15 minutes, follow the checks for each cause shown on the waiting screen. Show the steps that are not done in the same way as step 7.
62
94
 
63
- At the start of this session, the plugin hook sent the session to Harnessforce. Tell the user to open the Harnessforce Home in the browser (the URL in the environment variable `HARNESSFORCE_URL`) and confirm on the waiting screen for the first Run that it was received. If nothing arrives after 15 minutes, tell the user to follow the checks for each cause shown on the waiting screen. Finish with the following line.
95
+ If `harnessforce import` fails, show its output, go on, and show "Importing past sessions failed. Run `harnessforce import` later." before the last line. If `harnessforce status --json` fails, show its output and the Home URL, and still end with the line below: the settings were made in step 5, so not being able to read the status is not a failure of setup.
64
96
 
65
97
  > Harnessforce setup is complete.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harnessforce/claude-code-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Harnessforce plugin for Claude Code",
5
5
  "keywords": [
6
6
  "harnessforce",
@@ -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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
1021
- //#region ../../packages/agent-core/src/issue.ts
1003
+ // ../../packages/agent-core/src/issue.ts
1022
1004
  const isIssueIdentifier = (value) => isToken(value, 300);
1023
- //#endregion
1024
- //#region ../../packages/agent-core/src/using-harnessforce.ts
1025
- 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- 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 opts in with `--send-content` and the Workspace has opted in. They are not collected by default.\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- 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();
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 on your own. For work that commits changes, follow \"When no Issue is specified\" in `record-run` and ask the user once whether to link it to an existing Issue.\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
- //#endregion
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
- //#endregion
1074
- //#region src/input.ts
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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.2.0",
1998
+ version: "0.3.1",
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
@@ -9,13 +9,26 @@ Use the tools of the Harnessforce MCP server to link the work to the Issue. The
9
9
 
10
10
  ## At the start of the work
11
11
 
12
- 1. Pass the value of `harnessforce session_id: <session_id>` in the context as `session_id`, and register the Run with `start_run`. The Harnessforce plugin adds this line to the context at session start. If there are several, use the last one added. The input is the Issue identifier (`issue_identifier`), `agent` (`claude_code`), and `session_id`. If you know the repository, branch, and commit you are working on, pass them too. Keep the returned `run_id` until the end. If the line is missing from the context, follow "When the session ID is missing" below.
12
+ 1. Pass the value of `harnessforce session_id: <session_id>` in the context as `session_id`, and register the Run with `start_run`. The Harnessforce plugin adds this line to the context at session start. If there are several, use the last one added. The input is the Issue identifier (`issue_identifier`), `agent` (`claude_code`), and `session_id`. Do not pass the repository, branch, or commit: the hook registers the same session and fills them in. Keep the returned `run_id` until the end. If the line is missing from the context, follow "When the session ID is missing" below.
13
13
  2. Check the Issue with `get_issue`.
14
14
 
15
15
  ## When the session ID changes
16
16
 
17
17
  - If the value of the last `harnessforce session_id:` in the context differs from the value you passed to the previous `start_run` (for example after forking the conversation or running `/clear`), call `start_run` once more with the new value before continuing, and use the returned `run_id` for later `complete_run` calls.
18
18
 
19
+ ## When no Issue is specified
20
+
21
+ Ask the user once whether to link the work to an existing Issue, only when all of the following hold. Do not create an Issue: Issues are GitHub Issues.
22
+
23
+ - The user has not given an Issue identifier. If the current branch name contains an identifier (a branch name `gh-<number>` is passed as `<owner>/<repo>#<number>` of the repository) and `get_issue` resolves it, treat it as the specified Issue and do not ask. If `get_issue` returns `not_found`, treat it as no identifier.
24
+ - The work commits changes. Do not ask for research, answering questions, or conversations that only plan.
25
+ - The context has a `harnessforce session_id:` line.
26
+ - You have not asked yet for the last `harnessforce session_id:` value in the context. If the user declines, do not ask again while the value stays the same. After a fork or `/clear` changes the value, you may ask again.
27
+
28
+ To show the candidates, pick up to 3 words from the summary of the work that are likely to appear in an Issue title, and call `list_issues` once per word with `q` set to `-status:done,canceled <word>` and `sort` set to `updated`. Merge the results, remove duplicates, and show at most 5 Issues, most recently updated first. If the user picks one, call `start_run` with that Issue. If there is no candidate or the user picks none, continue the work without calling `start_run`.
29
+
30
+ If `start_run` returns `forbidden` (a view-only token or a view-only Workspace), tell the user the reason as it is, and do not ask again for that session ID.
31
+
19
32
  ## At the end of the work
20
33
 
21
34
  - Call `complete_run` with the `run_id`.
@@ -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 opts in with `--send-content` and the Workspace has opted in. They are not collected by default.
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
@@ -30,7 +32,7 @@ This session has the Harnessforce hook installed. Harnessforce links the work of
30
32
  - 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.
31
33
  - 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`.
32
34
  - Use `propose-improvements` for the analysis of `/harnessforce:tune`.
33
- - When no Issue is specified, do not start recording a Run.
35
+ - When no Issue is specified, do not start recording a Run on your own. For work that commits changes, follow "When no Issue is specified" in `record-run` and ask the user once whether to link it to an existing Issue.
34
36
 
35
37
  ## Red Flags
36
38