@harnessforce/cli 0.4.0 → 0.4.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.
- package/README.md +33 -20
- package/dist/bin.js +474 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
`harnessforce`, the command line of [Harnessforce](https://github.com/lambda-script/harnessforce-agent). It
|
|
4
4
|
connects a machine to a Harnessforce Workspace, supplies the ingest key to Claude Code's OpenTelemetry
|
|
5
|
-
exporter, launches an agent linked to an Issue, and imports the metadata of past Claude Code sessions.
|
|
5
|
+
exporter, launches an agent linked to an Issue, and imports the metadata of past Claude Code and Codex sessions.
|
|
6
6
|
|
|
7
7
|
It is used together with the `harnessforce` Claude Code plugin, whose `/harnessforce:setup` installs
|
|
8
|
-
it.
|
|
9
|
-
|
|
8
|
+
it. It is published on npm. To connect to a staging or local Harnessforce instead, install it from the
|
|
9
|
+
[local marketplace build](https://github.com/lambda-script/harnessforce-agent/blob/main/docs/runbooks/local-marketplace.md).
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -30,7 +30,7 @@ harnessforce init
|
|
|
30
30
|
| `harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]` | Logs in with the browser, stores a user ingest key and an API token in the keychain, and configures Claude Code and Codex |
|
|
31
31
|
| `harnessforce otel-headers` | Prints the `Authorization` header for Claude Code's `otelHeadersHelper`. Exits 3 when there is no key, 4 when the destination differs from the pinned origin, 5 while the CLI version is unsupported, and 1 on other failures. On Linux, after printing a key read from the kernel keyring, it moves that Workspace's items to an unlocked Secret Service (not when the hook starts it), so a reboot does not lose them |
|
|
32
32
|
| `harnessforce run --issue <identifier> -- <agent> [args]` | Launches an agent with its session linked to an Issue |
|
|
33
|
-
| `harnessforce import` | Sends the metadata of past Claude Code sessions in connected repositories |
|
|
33
|
+
| `harnessforce import` | Sends the metadata of past Claude Code and Codex sessions in connected repositories |
|
|
34
34
|
| `harnessforce status [--json]` | Shows the connected Workspace, your role, the setup steps that remain with their URLs, and whether the `origin` repository of the current directory is connected to the GitHub App. Reads only; the remote URL is compared on this machine and never sent. Exits 0 whenever the status was read |
|
|
35
35
|
| `harnessforce tune [--all] [--no-send] [--show-report] [--json]` | Analyzes past sessions on this machine by public rules and sends the counts (analysis reports) |
|
|
36
36
|
| `harnessforce tune --report-only [--all]` | Analyzes and sends like `harnessforce tune`, but writes nothing to stdout. Meant for your OS scheduler |
|
|
@@ -117,7 +117,9 @@ tokens are sent only to the origins pinned by `harnessforce init`.
|
|
|
117
117
|
1. It checks the OS keychain first: macOS Keychain or the Windows Credential Manager (through
|
|
118
118
|
[`@napi-rs/keyring`](https://github.com/Brooooooklyn/keyring-node)). On Linux it writes and deletes
|
|
119
119
|
a non-secret `availability-probe` item in the Secret Service (GNOME Keyring or KWallet), waiting at
|
|
120
|
-
most 60 seconds for you to unlock it, and saves there when that works.
|
|
120
|
+
most 60 seconds for you to unlock it, and saves there when that works. With KWallet it also says
|
|
121
|
+
which window asks for the password: KWallet shows one unlock window at a time, so an earlier
|
|
122
|
+
unanswered window behind other windows keeps the new one from appearing. Otherwise, as on a machine
|
|
121
123
|
reached only over SSH, it saves to the kernel's persistent keyring, which lives in memory and is
|
|
122
124
|
cleared on reboot or after a period of disuse; run `harnessforce init` again then. Without either
|
|
123
125
|
store it stops. Keys are never written to a plain file.
|
|
@@ -137,7 +139,9 @@ tokens are sent only to the origins pinned by `harnessforce init`.
|
|
|
137
139
|
them last, so a failure part way never pairs an old origin with a new key or token. After saving to
|
|
138
140
|
the Secret Service it deletes the same Workspace's items from the kernel keyring. Later commands read
|
|
139
141
|
a Workspace from the kernel keyring when it holds the ingest key there, and from the Secret Service
|
|
140
|
-
otherwise.
|
|
142
|
+
otherwise. Only `harnessforce init`, `run` and `import` ask a locked Secret Service for an unlock;
|
|
143
|
+
hooks and `harnessforce otel-headers` check the lock over D-Bus first and treat a locked item as
|
|
144
|
+
missing.
|
|
141
145
|
5. It updates the Claude Code user settings (`~/.claude/settings.json`, or `$CLAUDE_CONFIG_DIR`),
|
|
142
146
|
changing only these keys:
|
|
143
147
|
- `env`: the OTel exporter variables, `HARNESSFORCE_URL`, `HARNESSFORCE_ENDPOINT` and
|
|
@@ -153,7 +157,7 @@ tokens are sent only to the origins pinned by `harnessforce init`.
|
|
|
153
157
|
- `[otel].exporter`: OTLP/HTTP (`protocol = "binary"`) to the ingest endpoint with
|
|
154
158
|
`Authorization = "Bearer <user ingest key>"`. Codex cannot take the header from a command or expand
|
|
155
159
|
`${VAR}`, so this is the one place a key is written to a plain file. It is the user ingest key only
|
|
156
|
-
(never the API token), and you can revoke it
|
|
160
|
+
(never the API token), and you can revoke it in My ingest keys on your account page. `log_user_prompt` and
|
|
157
161
|
`log_agent_responses` are set to `true` for a category you send with `--send-content` and the
|
|
158
162
|
Workspace stores, and `false` otherwise
|
|
159
163
|
- `[[hooks.SessionStart]]`: a handler that runs `harnessforce hook session-start`
|
|
@@ -170,12 +174,13 @@ tokens are sent only to the origins pinned by `harnessforce init`.
|
|
|
170
174
|
`harnessforce otel-headers` prints `{"Authorization":"Bearer <key>"}`. The key is `HARNESSFORCE_INGEST_KEY` from
|
|
171
175
|
the managed settings file when that is set, and otherwise the user key for
|
|
172
176
|
`HARNESSFORCE_WORKSPACE_ID` from the keychain. If
|
|
173
|
-
there is no key, it prints nothing and exits
|
|
177
|
+
there is no key, it prints nothing and exits 3. The user key is printed only when every destination
|
|
174
178
|
variable that is set (`HARNESSFORCE_ENDPOINT`, `OTEL_EXPORTER_OTLP_ENDPOINT` and the per-signal
|
|
175
179
|
`OTEL_EXPORTER_OTLP_*_ENDPOINT`) has the pinned origin. This stops a repository's
|
|
176
180
|
`.claude/settings.json` `env` from pointing the hook at another host to collect the key. Otherwise it
|
|
177
|
-
writes `harnessforce: user key withheld (destination not verified)` to stderr and exits
|
|
178
|
-
`harnessforce init` again to re-pin.
|
|
181
|
+
writes `harnessforce: user key withheld (destination not verified)` to stderr and exits 4. Run
|
|
182
|
+
`harnessforce init` again to re-pin. While the CLI version is unsupported it exits 5 without reading
|
|
183
|
+
the keychain, and any other failure (such as an unavailable keychain) exits 1.
|
|
179
184
|
|
|
180
185
|
### Refreshing the API token
|
|
181
186
|
|
|
@@ -242,15 +247,23 @@ It exits with the agent's exit code. Prompt and body logging are never turned on
|
|
|
242
247
|
|
|
243
248
|
## Importing past sessions (`harnessforce import`)
|
|
244
249
|
|
|
245
|
-
`harnessforce import` sends metadata of past Claude Code sessions on this machine to Harnessforce,
|
|
246
|
-
work done before the plugin was installed can be linked to issues too. Run `harnessforce init` first.
|
|
247
|
-
|
|
248
|
-
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
`harnessforce import` sends metadata of past Claude Code and Codex sessions on this machine to Harnessforce,
|
|
251
|
+
so that work done before the plugin was installed can be linked to issues too. Run `harnessforce init` first.
|
|
252
|
+
|
|
253
|
+
- For Claude Code it reads the transcripts in `~/.claude/projects/<project>/<session>.jsonl`, or under
|
|
254
|
+
the `CLAUDE_CONFIG_DIR` set in the `env` of the managed settings file or of `~/.claude/settings.json`.
|
|
255
|
+
The `CLAUDE_CONFIG_DIR` of the process environment is not read, because a repository's
|
|
256
|
+
`.claude/settings.json` can change it.
|
|
257
|
+
- For Codex it reads the rollouts in `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. `CODEX_HOME` is not
|
|
258
|
+
read, for the same reason. Compressed rollouts (`.jsonl.zst`) and `archived_sessions` are not read; the
|
|
259
|
+
compressed ones in the import window are counted and reported.
|
|
260
|
+
- The formats are not documented, so the parsers are versioned (`parser_version`), and unreadable lines
|
|
261
|
+
and files are skipped and counted.
|
|
262
|
+
- Only these values are sent: agent, session ID, start and end time, repository, branch, the most used
|
|
263
|
+
model, input and output token counts (without cache tokens), and the number of calls per tool. Claude
|
|
264
|
+
Code sessions also send the first prompt ID and the number of failures per tool. Codex sessions also
|
|
265
|
+
send the commit the session started on, and no failure counts, because a rollout does not keep whether
|
|
266
|
+
every call succeeded. Prompts, responses and tool inputs and outputs are never sent.
|
|
254
267
|
- Only sessions whose `cwd` is in a repository connected to the Workspace are sent. The connected
|
|
255
268
|
repositories come from `GET <HARNESSFORCE_URL>/api/v1/repositories` (all pages) and the import
|
|
256
269
|
window from `session_import_days` (`GET <HARNESSFORCE_URL>/api/v1/workspace`), both with the API
|
|
@@ -286,7 +299,7 @@ role cannot use it.
|
|
|
286
299
|
commands, paths and error text never leave the machine.
|
|
287
300
|
- By default it analyzes sessions started in the last 30 days in connected repositories. `--all`
|
|
288
301
|
analyzes every session, but still sends only sessions in connected repositories that started within
|
|
289
|
-
`session_import_days`. Sessions
|
|
302
|
+
`session_import_days`. Sessions that were not imported yet are imported first, like `harnessforce import`.
|
|
290
303
|
- Sending is fail-open: when Harnessforce cannot be reached, the reports stay in
|
|
291
304
|
`~/.harnessforce/tune/unsent.json` and are sent by the next run. `401` and `403` delete the unsent
|
|
292
305
|
reports and ask you to run `harnessforce init`.
|
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { access, chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
3
4
|
import { constants, homedir, tmpdir } from "node:os";
|
|
4
5
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
5
6
|
import { basename, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
6
7
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
|
-
import { access, chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
8
8
|
import { execFile, spawn } from "node:child_process";
|
|
9
9
|
import { constants as constants$1, createReadStream } from "node:fs";
|
|
10
|
+
import { connect } from "node:net";
|
|
10
11
|
import { createServer } from "node:http";
|
|
11
12
|
import { ATTR, INTERVENTION_KINDS, LOOP_KINDS, MAX_WORK_LOCATIONS, PROPOSAL_KINDS, isToken, normalizeRepository } from "@harnessforce/semconv";
|
|
12
13
|
import { createInterface } from "node:readline";
|
|
@@ -434,6 +435,7 @@ const INIT_MESSAGES = {
|
|
|
434
435
|
contentNotStored: "This workspace does not store content, so sending content was not enabled. An owner or admin can turn it on under Collection & retention in the workspace settings, then run `harnessforce init --send-content` again",
|
|
435
436
|
contentNotSent: "Content is not sent from this machine. Run `harnessforce init --send-content` to send it"
|
|
436
437
|
};
|
|
438
|
+
const kwalletUnlockHint = (walletLabel = "kdewallet") => `KWallet asks for the password of the wallet "${walletLabel}" in a "KDE Wallet Service" window. If no window appears, an earlier request may be waiting behind other windows: answer that window, or log out and back in, then run \`harnessforce init\` again`;
|
|
437
439
|
const codexNotes = (configPath) => [
|
|
438
440
|
`Wrote the Codex configuration (${configPath}). Restart Codex to apply it`,
|
|
439
441
|
"Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart, PostToolUse, and Stop hooks in /hooks",
|
|
@@ -479,6 +481,7 @@ const apiTokenWorkspaceId = (account) => account.endsWith(API_TOKEN_SUFFIX) ? ac
|
|
|
479
481
|
// src/credentials/linux-keychain.ts
|
|
480
482
|
const SECRET_SERVICE_TIMEOUT_MS = 6e4;
|
|
481
483
|
const WAITING_NOTICE_DELAY_MS = 2e3;
|
|
484
|
+
const PROVIDER_TIMEOUT_MS = 500;
|
|
482
485
|
const PROBE_ACCOUNT$1 = "availability-probe";
|
|
483
486
|
const PROBE_VALUE = "harnessforce";
|
|
484
487
|
const MIGRATION_PROBE_TIMEOUT_MS = 200;
|
|
@@ -487,10 +490,11 @@ const CLEANUP_TIMEOUT_MS = 200;
|
|
|
487
490
|
const succeeds = (operation) => operation().then(() => true, () => false);
|
|
488
491
|
const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
|
|
489
492
|
function createLinuxKeychain(options) {
|
|
490
|
-
return build(options, SECRET_SERVICE_TIMEOUT_MS);
|
|
493
|
+
return build(options, SECRET_SERVICE_TIMEOUT_MS, true);
|
|
491
494
|
}
|
|
492
|
-
function build(options, timeoutMs) {
|
|
493
|
-
const { kernel
|
|
495
|
+
function build(options, timeoutMs, allowsUnlockPrompt) {
|
|
496
|
+
const { kernel } = options;
|
|
497
|
+
const secretService = allowsUnlockPrompt ? options.secretService : options.secretService.withoutUnlockPrompt();
|
|
494
498
|
const locations = /* @__PURE__ */ new Map();
|
|
495
499
|
const locate = (workspaceId) => {
|
|
496
500
|
let located = locations.get(workspaceId);
|
|
@@ -535,7 +539,7 @@ function build(options, timeoutMs) {
|
|
|
535
539
|
if (location !== "kernel") return;
|
|
536
540
|
await migrate(options, workspaceId).catch(() => {});
|
|
537
541
|
},
|
|
538
|
-
withTimeout: (ms) => build(options, ms),
|
|
542
|
+
withTimeout: (ms) => build(options, ms, false),
|
|
539
543
|
openForInit: (init) => openForInit(options, timeoutMs, init)
|
|
540
544
|
};
|
|
541
545
|
}
|
|
@@ -544,8 +548,17 @@ async function openForInit({ kernel, secretService }, timeoutMs, { previousWorks
|
|
|
544
548
|
await store.set(PROBE_ACCOUNT$1, PROBE_VALUE, timeoutMs);
|
|
545
549
|
await store.delete(PROBE_ACCOUNT$1, timeoutMs);
|
|
546
550
|
});
|
|
547
|
-
|
|
548
|
-
const
|
|
551
|
+
let isWaiting = true;
|
|
552
|
+
const notify = (provider) => {
|
|
553
|
+
if (isWaiting) onWaiting(provider);
|
|
554
|
+
};
|
|
555
|
+
const notice = setTimeout(() => {
|
|
556
|
+
secretService.provider(PROVIDER_TIMEOUT_MS).then(notify, () => notify(void 0));
|
|
557
|
+
}, WAITING_NOTICE_DELAY_MS);
|
|
558
|
+
const usesSecretService = await canWrite(secretService).finally(() => {
|
|
559
|
+
isWaiting = false;
|
|
560
|
+
clearTimeout(notice);
|
|
561
|
+
});
|
|
549
562
|
const readKernel = async () => {
|
|
550
563
|
if (!previousWorkspaceId) return [];
|
|
551
564
|
const accounts = [ingestKeyAccount(previousWorkspaceId), apiTokenAccount(previousWorkspaceId)];
|
|
@@ -584,7 +597,8 @@ const MIGRATION_ORDER = (workspaceId) => [
|
|
|
584
597
|
apiTokenAccount(workspaceId),
|
|
585
598
|
ingestKeyAccount(workspaceId)
|
|
586
599
|
];
|
|
587
|
-
async function migrate({ kernel, secretService }, workspaceId) {
|
|
600
|
+
async function migrate({ kernel, secretService: unlimited }, workspaceId) {
|
|
601
|
+
const secretService = unlimited.withoutUnlockPrompt();
|
|
588
602
|
let isExpired = false;
|
|
589
603
|
const deadline = Date.now() + MIGRATION_BUDGET_MS;
|
|
590
604
|
const remaining = () => {
|
|
@@ -712,6 +726,419 @@ const unsupported = {
|
|
|
712
726
|
withTimeout: () => unsupported,
|
|
713
727
|
openForInit: async () => void 0
|
|
714
728
|
};
|
|
729
|
+
// src/credentials/dbus.ts
|
|
730
|
+
const TYPES = [
|
|
731
|
+
"",
|
|
732
|
+
"method_call",
|
|
733
|
+
"method_return",
|
|
734
|
+
"error",
|
|
735
|
+
"signal"
|
|
736
|
+
];
|
|
737
|
+
const FIELDS = {
|
|
738
|
+
path: [1, "o"],
|
|
739
|
+
interface: [2, "s"],
|
|
740
|
+
member: [3, "s"],
|
|
741
|
+
errorName: [4, "s"],
|
|
742
|
+
replySerial: [5, "u"],
|
|
743
|
+
destination: [6, "s"],
|
|
744
|
+
sender: [7, "s"],
|
|
745
|
+
signature: [8, "g"]
|
|
746
|
+
};
|
|
747
|
+
function parseSignature(signature) {
|
|
748
|
+
let index = 0;
|
|
749
|
+
const one = () => {
|
|
750
|
+
const code = signature[index++] ?? "";
|
|
751
|
+
if (code === "a") return {
|
|
752
|
+
code,
|
|
753
|
+
children: [one()]
|
|
754
|
+
};
|
|
755
|
+
if (code === "(" || code === "{") {
|
|
756
|
+
const close = code === "(" ? ")" : "}";
|
|
757
|
+
const children = [];
|
|
758
|
+
while (signature[index] !== close) {
|
|
759
|
+
if (index >= signature.length) throw new Error("bad signature");
|
|
760
|
+
children.push(one());
|
|
761
|
+
}
|
|
762
|
+
index++;
|
|
763
|
+
return {
|
|
764
|
+
code,
|
|
765
|
+
children
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
if (!"ybusogv".includes(code) || code === "") throw new Error(`unsupported D-Bus type ${code}`);
|
|
769
|
+
return {
|
|
770
|
+
code,
|
|
771
|
+
children: []
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
const types = [];
|
|
775
|
+
while (index < signature.length) types.push(one());
|
|
776
|
+
return types;
|
|
777
|
+
}
|
|
778
|
+
const alignmentOf = (type) => ({
|
|
779
|
+
y: 1,
|
|
780
|
+
g: 1,
|
|
781
|
+
v: 1,
|
|
782
|
+
b: 4,
|
|
783
|
+
u: 4,
|
|
784
|
+
s: 4,
|
|
785
|
+
o: 4,
|
|
786
|
+
a: 4
|
|
787
|
+
})[type.code] ?? 8;
|
|
788
|
+
var Writer = class {
|
|
789
|
+
bytes = [];
|
|
790
|
+
get length() {
|
|
791
|
+
return this.bytes.length;
|
|
792
|
+
}
|
|
793
|
+
align(n) {
|
|
794
|
+
while (this.bytes.length % n !== 0) this.bytes.push(0);
|
|
795
|
+
}
|
|
796
|
+
uint32(value) {
|
|
797
|
+
this.align(4);
|
|
798
|
+
const buffer = Buffer.alloc(4);
|
|
799
|
+
buffer.writeUInt32LE(value);
|
|
800
|
+
this.bytes.push(...buffer);
|
|
801
|
+
}
|
|
802
|
+
patchUint32(offset, value) {
|
|
803
|
+
const buffer = Buffer.alloc(4);
|
|
804
|
+
buffer.writeUInt32LE(value);
|
|
805
|
+
this.bytes.splice(offset, 4, ...buffer);
|
|
806
|
+
}
|
|
807
|
+
raw(values) {
|
|
808
|
+
this.bytes.push(...values);
|
|
809
|
+
}
|
|
810
|
+
write(type, value) {
|
|
811
|
+
const [child] = type.children;
|
|
812
|
+
switch (type.code) {
|
|
813
|
+
case "y":
|
|
814
|
+
this.raw([Number(value)]);
|
|
815
|
+
return;
|
|
816
|
+
case "b":
|
|
817
|
+
this.uint32(value ? 1 : 0);
|
|
818
|
+
return;
|
|
819
|
+
case "u":
|
|
820
|
+
this.uint32(Number(value));
|
|
821
|
+
return;
|
|
822
|
+
case "s":
|
|
823
|
+
case "o": {
|
|
824
|
+
const text = Buffer.from(String(value), "utf8");
|
|
825
|
+
this.uint32(text.length);
|
|
826
|
+
this.raw([...text, 0]);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
case "g": {
|
|
830
|
+
const text = Buffer.from(String(value), "utf8");
|
|
831
|
+
this.raw([
|
|
832
|
+
text.length,
|
|
833
|
+
...text,
|
|
834
|
+
0
|
|
835
|
+
]);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
case "v": {
|
|
839
|
+
const variant = value;
|
|
840
|
+
this.write({
|
|
841
|
+
code: "g",
|
|
842
|
+
children: []
|
|
843
|
+
}, variant.signature);
|
|
844
|
+
const [inner] = parseSignature(variant.signature);
|
|
845
|
+
if (!inner) throw new Error("empty variant");
|
|
846
|
+
this.write(inner, variant.value);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
case "a": {
|
|
850
|
+
if (!child) throw new Error("bad array");
|
|
851
|
+
this.uint32(0);
|
|
852
|
+
const lengthAt = this.length - 4;
|
|
853
|
+
this.align(alignmentOf(child));
|
|
854
|
+
const start = this.length;
|
|
855
|
+
const elements = child.code === "{" ? Object.entries(value) : value;
|
|
856
|
+
for (const element of elements) this.write(child, element);
|
|
857
|
+
this.patchUint32(lengthAt, this.length - start);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
default: {
|
|
861
|
+
this.align(8);
|
|
862
|
+
const fields = value;
|
|
863
|
+
type.children.forEach((field, i) => {
|
|
864
|
+
this.write(field, fields[i]);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
toBuffer() {
|
|
870
|
+
return Buffer.from(this.bytes);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
var Reader = class {
|
|
874
|
+
offset = 0;
|
|
875
|
+
buffer;
|
|
876
|
+
isLittleEndian;
|
|
877
|
+
constructor(buffer, isLittleEndian) {
|
|
878
|
+
this.buffer = buffer;
|
|
879
|
+
this.isLittleEndian = isLittleEndian;
|
|
880
|
+
}
|
|
881
|
+
align(n) {
|
|
882
|
+
this.offset = Math.ceil(this.offset / n) * n;
|
|
883
|
+
}
|
|
884
|
+
uint32() {
|
|
885
|
+
this.align(4);
|
|
886
|
+
const value = this.isLittleEndian ? this.buffer.readUInt32LE(this.offset) : this.buffer.readUInt32BE(this.offset);
|
|
887
|
+
this.offset += 4;
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
read(type) {
|
|
891
|
+
const [child] = type.children;
|
|
892
|
+
switch (type.code) {
|
|
893
|
+
case "y": return this.buffer[this.offset++];
|
|
894
|
+
case "b": return this.uint32() !== 0;
|
|
895
|
+
case "u": return this.uint32();
|
|
896
|
+
case "s":
|
|
897
|
+
case "o": {
|
|
898
|
+
const length = this.uint32();
|
|
899
|
+
const text = this.buffer.toString("utf8", this.offset, this.offset + length);
|
|
900
|
+
this.offset += length + 1;
|
|
901
|
+
return text;
|
|
902
|
+
}
|
|
903
|
+
case "g": {
|
|
904
|
+
const length = this.buffer[this.offset++] ?? 0;
|
|
905
|
+
const text = this.buffer.toString("utf8", this.offset, this.offset + length);
|
|
906
|
+
this.offset += length + 1;
|
|
907
|
+
return text;
|
|
908
|
+
}
|
|
909
|
+
case "v": {
|
|
910
|
+
const signature = this.read({
|
|
911
|
+
code: "g",
|
|
912
|
+
children: []
|
|
913
|
+
});
|
|
914
|
+
const [inner] = parseSignature(signature);
|
|
915
|
+
if (!inner) throw new Error("empty variant");
|
|
916
|
+
return {
|
|
917
|
+
signature,
|
|
918
|
+
value: this.read(inner)
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
case "a": {
|
|
922
|
+
if (!child) throw new Error("bad array");
|
|
923
|
+
const length = this.uint32();
|
|
924
|
+
this.align(alignmentOf(child));
|
|
925
|
+
const end = this.offset + length;
|
|
926
|
+
const elements = [];
|
|
927
|
+
while (this.offset < end) elements.push(this.read(child));
|
|
928
|
+
return child.code === "{" ? Object.fromEntries(elements) : elements;
|
|
929
|
+
}
|
|
930
|
+
default:
|
|
931
|
+
this.align(8);
|
|
932
|
+
return type.children.map((field) => this.read(field));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const HEADER_FIELDS = parseSignature("a(yv)")[0];
|
|
937
|
+
function encodeMessage(message) {
|
|
938
|
+
const signature = message.signature ?? "";
|
|
939
|
+
const bodyWriter = new Writer();
|
|
940
|
+
parseSignature(signature).forEach((type, i) => {
|
|
941
|
+
bodyWriter.write(type, message.body?.[i]);
|
|
942
|
+
});
|
|
943
|
+
const body = bodyWriter.toBuffer();
|
|
944
|
+
const fields = [];
|
|
945
|
+
for (const [name, [code, fieldType]] of Object.entries(FIELDS)) {
|
|
946
|
+
const value = message[name];
|
|
947
|
+
if (value === void 0 || name === "signature" && value === "") continue;
|
|
948
|
+
fields.push([code, {
|
|
949
|
+
signature: fieldType,
|
|
950
|
+
value
|
|
951
|
+
}]);
|
|
952
|
+
}
|
|
953
|
+
const header = new Writer();
|
|
954
|
+
header.raw([
|
|
955
|
+
"l".charCodeAt(0),
|
|
956
|
+
TYPES.indexOf(message.type),
|
|
957
|
+
0,
|
|
958
|
+
1
|
|
959
|
+
]);
|
|
960
|
+
header.uint32(body.length);
|
|
961
|
+
header.uint32(message.serial);
|
|
962
|
+
header.write(HEADER_FIELDS, fields);
|
|
963
|
+
header.align(8);
|
|
964
|
+
return Buffer.concat([header.toBuffer(), body]);
|
|
965
|
+
}
|
|
966
|
+
function decodeMessages(buffer) {
|
|
967
|
+
const messages = [];
|
|
968
|
+
let rest = buffer;
|
|
969
|
+
while (rest.length >= 16) {
|
|
970
|
+
const isLittleEndian = rest[0] === "l".charCodeAt(0);
|
|
971
|
+
const readUint32 = (offset) => isLittleEndian ? rest.readUInt32LE(offset) : rest.readUInt32BE(offset);
|
|
972
|
+
const bodyLength = readUint32(4);
|
|
973
|
+
const fieldsLength = readUint32(12);
|
|
974
|
+
const headerLength = Math.ceil((16 + fieldsLength) / 8) * 8;
|
|
975
|
+
const total = headerLength + bodyLength;
|
|
976
|
+
if (rest.length < total) break;
|
|
977
|
+
const reader = new Reader(rest.subarray(0, total), isLittleEndian);
|
|
978
|
+
reader.offset = 8;
|
|
979
|
+
const serial = reader.uint32();
|
|
980
|
+
const fields = reader.read(HEADER_FIELDS);
|
|
981
|
+
const message = {
|
|
982
|
+
type: TYPES[rest[1] ?? 0] || "signal",
|
|
983
|
+
serial
|
|
984
|
+
};
|
|
985
|
+
for (const [code, { value }] of fields) {
|
|
986
|
+
const name = Object.entries(FIELDS).find(([, [c]]) => c === code)?.[0];
|
|
987
|
+
if (name) Object.assign(message, { [name]: value });
|
|
988
|
+
}
|
|
989
|
+
reader.offset = headerLength;
|
|
990
|
+
message.body = parseSignature(message.signature ?? "").map((type) => reader.read(type));
|
|
991
|
+
messages.push(message);
|
|
992
|
+
rest = rest.subarray(total);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
messages,
|
|
996
|
+
rest
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function socketPath(address) {
|
|
1000
|
+
for (const entry of address.split(";")) {
|
|
1001
|
+
const [transport, rest = ""] = entry.split(/:(.*)/s);
|
|
1002
|
+
if (transport !== "unix") continue;
|
|
1003
|
+
const params = new Map(rest.split(",").map((pair) => pair.split(/=(.*)/s)));
|
|
1004
|
+
const path = params.get("path");
|
|
1005
|
+
if (path) return decodeURIComponent(path);
|
|
1006
|
+
const abstract = params.get("abstract");
|
|
1007
|
+
if (abstract) return `\0${decodeURIComponent(abstract)}`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function openBus(address, uid, timeoutMs) {
|
|
1011
|
+
const path = socketPath(address);
|
|
1012
|
+
if (!path) return Promise.reject(/* @__PURE__ */ new Error("unsupported D-Bus address"));
|
|
1013
|
+
return new Promise((resolve, reject) => {
|
|
1014
|
+
const socket = connect({ path });
|
|
1015
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
1016
|
+
let serial = 0;
|
|
1017
|
+
let pending = Buffer.alloc(0);
|
|
1018
|
+
let isAuthenticated = false;
|
|
1019
|
+
const fail = (error) => {
|
|
1020
|
+
socket.destroy();
|
|
1021
|
+
reject(error);
|
|
1022
|
+
for (const call of waiting.values()) call.reject(error);
|
|
1023
|
+
waiting.clear();
|
|
1024
|
+
};
|
|
1025
|
+
const timer = setTimeout(() => fail(/* @__PURE__ */ new Error("D-Bus timed out")), timeoutMs);
|
|
1026
|
+
const close = () => {
|
|
1027
|
+
clearTimeout(timer);
|
|
1028
|
+
socket.destroy();
|
|
1029
|
+
};
|
|
1030
|
+
const call = (request) => new Promise((resolveCall, rejectCall) => {
|
|
1031
|
+
serial += 1;
|
|
1032
|
+
waiting.set(serial, {
|
|
1033
|
+
resolve: resolveCall,
|
|
1034
|
+
reject: rejectCall
|
|
1035
|
+
});
|
|
1036
|
+
socket.write(encodeMessage({
|
|
1037
|
+
type: "method_call",
|
|
1038
|
+
serial,
|
|
1039
|
+
...request
|
|
1040
|
+
}));
|
|
1041
|
+
});
|
|
1042
|
+
socket.on("error", fail);
|
|
1043
|
+
socket.on("close", () => fail(/* @__PURE__ */ new Error("D-Bus connection closed")));
|
|
1044
|
+
socket.on("connect", () => {
|
|
1045
|
+
const uidHex = Buffer.from(String(uid)).toString("hex");
|
|
1046
|
+
socket.write(`\0AUTH EXTERNAL ${uidHex}\r\n`);
|
|
1047
|
+
});
|
|
1048
|
+
socket.on("data", (chunk) => {
|
|
1049
|
+
pending = Buffer.concat([pending, chunk]);
|
|
1050
|
+
if (!isAuthenticated) {
|
|
1051
|
+
const lineEnd = pending.indexOf("\r\n");
|
|
1052
|
+
if (lineEnd < 0) return;
|
|
1053
|
+
const line = pending.toString("latin1", 0, lineEnd);
|
|
1054
|
+
pending = pending.subarray(lineEnd + 2);
|
|
1055
|
+
if (!line.startsWith("OK ")) return fail(/* @__PURE__ */ new Error("D-Bus authentication failed"));
|
|
1056
|
+
isAuthenticated = true;
|
|
1057
|
+
socket.write("BEGIN\r\n");
|
|
1058
|
+
call({
|
|
1059
|
+
destination: "org.freedesktop.DBus",
|
|
1060
|
+
path: "/org/freedesktop/DBus",
|
|
1061
|
+
interface: "org.freedesktop.DBus",
|
|
1062
|
+
member: "Hello"
|
|
1063
|
+
}).then(() => resolve({
|
|
1064
|
+
call,
|
|
1065
|
+
close
|
|
1066
|
+
}), fail);
|
|
1067
|
+
}
|
|
1068
|
+
const { messages, rest } = decodeMessages(pending);
|
|
1069
|
+
pending = rest;
|
|
1070
|
+
for (const message of messages) {
|
|
1071
|
+
const replied = message.replySerial === void 0 ? void 0 : waiting.get(message.replySerial);
|
|
1072
|
+
if (!replied || message.replySerial === void 0) continue;
|
|
1073
|
+
waiting.delete(message.replySerial);
|
|
1074
|
+
if (message.type === "error") replied.reject(new Error(message.errorName ?? "D-Bus error"));
|
|
1075
|
+
else replied.resolve(message.body ?? []);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
// src/credentials/secret-service-lock.ts
|
|
1081
|
+
const SECRETS = "org.freedesktop.secrets";
|
|
1082
|
+
const SERVICE_PATH = "/org/freedesktop/secrets";
|
|
1083
|
+
const DEFAULT_COLLECTION = "/org/freedesktop/secrets/aliases/default";
|
|
1084
|
+
const KWALLET_PROCESS = /^(ksecretd|kwalletd\d*)$/;
|
|
1085
|
+
function sessionBusAddress(env) {
|
|
1086
|
+
if (env.DBUS_SESSION_BUS_ADDRESS) return env.DBUS_SESSION_BUS_ADDRESS;
|
|
1087
|
+
if (env.XDG_RUNTIME_DIR) return `unix:path=${env.XDG_RUNTIME_DIR}/bus`;
|
|
1088
|
+
}
|
|
1089
|
+
const property = (bus, name) => bus.call({
|
|
1090
|
+
destination: SECRETS,
|
|
1091
|
+
path: DEFAULT_COLLECTION,
|
|
1092
|
+
interface: "org.freedesktop.DBus.Properties",
|
|
1093
|
+
member: "Get",
|
|
1094
|
+
signature: "ss",
|
|
1095
|
+
body: ["org.freedesktop.Secret.Collection", name]
|
|
1096
|
+
}).then(([variant]) => variant.value);
|
|
1097
|
+
function createSecretServiceLock({ address, uid, readComm }) {
|
|
1098
|
+
const withBus = async (timeoutMs, use) => {
|
|
1099
|
+
if (!address) throw new Error("no D-Bus session bus");
|
|
1100
|
+
const bus = await openBus(address, uid, timeoutMs);
|
|
1101
|
+
try {
|
|
1102
|
+
return await use(bus);
|
|
1103
|
+
} finally {
|
|
1104
|
+
bus.close();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
return {
|
|
1108
|
+
needsUnlock: (operation, account, timeoutMs) => withBus(timeoutMs, async (bus) => {
|
|
1109
|
+
const [unlocked, locked] = await bus.call({
|
|
1110
|
+
destination: SECRETS,
|
|
1111
|
+
path: SERVICE_PATH,
|
|
1112
|
+
interface: "org.freedesktop.Secret.Service",
|
|
1113
|
+
member: "SearchItems",
|
|
1114
|
+
signature: "a{ss}",
|
|
1115
|
+
body: [{
|
|
1116
|
+
service: KEYCHAIN_SERVICE,
|
|
1117
|
+
username: account
|
|
1118
|
+
}]
|
|
1119
|
+
});
|
|
1120
|
+
if (locked.length > 0) return true;
|
|
1121
|
+
if (unlocked.length > 0 || operation !== "set") return false;
|
|
1122
|
+
return property(bus, "Locked").then((isLocked) => isLocked === true, () => true);
|
|
1123
|
+
}),
|
|
1124
|
+
provider: (timeoutMs) => withBus(timeoutMs, async (bus) => {
|
|
1125
|
+
const [pid] = await bus.call({
|
|
1126
|
+
destination: "org.freedesktop.DBus",
|
|
1127
|
+
path: "/org/freedesktop/DBus",
|
|
1128
|
+
interface: "org.freedesktop.DBus",
|
|
1129
|
+
member: "GetConnectionUnixProcessID",
|
|
1130
|
+
signature: "s",
|
|
1131
|
+
body: [SECRETS]
|
|
1132
|
+
});
|
|
1133
|
+
const comm = (await readComm(pid)).trim();
|
|
1134
|
+
const label = await property(bus, "Label").catch(() => void 0);
|
|
1135
|
+
return {
|
|
1136
|
+
isKWallet: KWALLET_PROCESS.test(comm),
|
|
1137
|
+
walletLabel: typeof label === "string" ? label : void 0
|
|
1138
|
+
};
|
|
1139
|
+
})
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
715
1142
|
// src/credentials/secret-service-process.ts
|
|
716
1143
|
const CHILD_SCRIPT = `
|
|
717
1144
|
const [op, service, account, timeoutMs, modulePath] = process.argv.slice(1);
|
|
@@ -737,7 +1164,7 @@ process.stdin.on("end", async () => {
|
|
|
737
1164
|
}
|
|
738
1165
|
});
|
|
739
1166
|
`;
|
|
740
|
-
function createSecretServiceProcess({ execPath, resolveKeyring }) {
|
|
1167
|
+
function createSecretServiceProcess({ execPath, resolveKeyring, lock }) {
|
|
741
1168
|
const run = (op, account, timeoutMs, input = "") => new Promise((resolve, reject) => {
|
|
742
1169
|
const child = spawn(execPath, [
|
|
743
1170
|
"-e",
|
|
@@ -768,17 +1195,29 @@ function createSecretServiceProcess({ execPath, resolveKeyring }) {
|
|
|
768
1195
|
child.stdin.on("error", () => {});
|
|
769
1196
|
child.stdin.end(input);
|
|
770
1197
|
});
|
|
771
|
-
|
|
1198
|
+
const operations = (perform) => ({
|
|
772
1199
|
async get(account, timeoutMs) {
|
|
773
|
-
const secret = await
|
|
1200
|
+
const secret = await perform("get", account, timeoutMs);
|
|
774
1201
|
return typeof secret === "string" ? secret : void 0;
|
|
775
1202
|
},
|
|
776
1203
|
async set(account, secret, timeoutMs) {
|
|
777
|
-
await
|
|
1204
|
+
await perform("set", account, timeoutMs, secret);
|
|
778
1205
|
},
|
|
779
1206
|
async delete(account, timeoutMs) {
|
|
780
|
-
await
|
|
781
|
-
}
|
|
1207
|
+
await perform("delete", account, timeoutMs);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
const withoutPrompt = async (op, account, timeoutMs, input) => {
|
|
1211
|
+
const startedAt = Date.now();
|
|
1212
|
+
if (await lock.needsUnlock(op, account, timeoutMs)) throw new Error("the Secret Service is locked");
|
|
1213
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
1214
|
+
if (remainingMs <= 0) throw new Error("Secret Service timed out");
|
|
1215
|
+
return run(op, account, remainingMs, input);
|
|
1216
|
+
};
|
|
1217
|
+
return {
|
|
1218
|
+
...operations(run),
|
|
1219
|
+
withoutUnlockPrompt: () => operations(withoutPrompt),
|
|
1220
|
+
provider: (timeoutMs) => lock.provider(timeoutMs),
|
|
782
1221
|
async list(timeoutMs) {
|
|
783
1222
|
return (await run("list", "", timeoutMs)).map(({ account, password }) => ({
|
|
784
1223
|
account,
|
|
@@ -1432,8 +1871,10 @@ What it does:
|
|
|
1432
1871
|
and what to do next.
|
|
1433
1872
|
|
|
1434
1873
|
Sends:
|
|
1435
|
-
Per session: agent, session ID, repository, branch,
|
|
1436
|
-
|
|
1874
|
+
Per session: agent, session ID, repository, branch, start and end time,
|
|
1875
|
+
model, input and output token counts, and tool call counts. Claude Code
|
|
1876
|
+
sessions also send the first prompt ID; Codex sessions also send the commit
|
|
1877
|
+
the session started on.
|
|
1437
1878
|
|
|
1438
1879
|
Does not send:
|
|
1439
1880
|
Prompt text, model responses, tool input or output, and code diffs. Sessions
|
|
@@ -1498,8 +1939,12 @@ Does not send:
|
|
|
1498
1939
|
|
|
1499
1940
|
Exit codes:
|
|
1500
1941
|
0 Header printed.
|
|
1501
|
-
|
|
1502
|
-
|
|
1942
|
+
3 No key for the Workspace (not logged in, or the keychain item is gone, for
|
|
1943
|
+
example after a reboot cleared the kernel keyring).
|
|
1944
|
+
4 The destination was not verified: it differs from the origin init pinned.
|
|
1945
|
+
5 This CLI version is unsupported. The keychain is not read.
|
|
1946
|
+
1 Any other failure (keychain unavailable, read failed, or timed out).
|
|
1947
|
+
Nothing is printed to stdout unless the exit code is 0.
|
|
1503
1948
|
|
|
1504
1949
|
Examples:
|
|
1505
1950
|
harnessforce otel-headers
|
|
@@ -1717,7 +2162,7 @@ const DESTINATION_MISMATCH_WARNING = {
|
|
|
1717
2162
|
// ../agent-core/src/issue.ts
|
|
1718
2163
|
const isIssueIdentifier = (value) => isToken(value, 300);
|
|
1719
2164
|
// ../agent-core/src/using-harnessforce.ts
|
|
1720
|
-
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();
|
|
2165
|
+
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();
|
|
1721
2166
|
// ../agent-core/src/session-context.ts
|
|
1722
2167
|
const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
|
|
1723
2168
|
const sessionContext = (sessionId) => ({ hookSpecificOutput: {
|
|
@@ -10151,7 +10596,10 @@ function finishInit(issued, args, deps, result) {
|
|
|
10151
10596
|
async function openKeychain(deps, previousWorkspaceId) {
|
|
10152
10597
|
return await deps.keychain.openForInit({
|
|
10153
10598
|
previousWorkspaceId,
|
|
10154
|
-
onWaiting: () =>
|
|
10599
|
+
onWaiting: (provider) => {
|
|
10600
|
+
deps.stderr(`${INIT_MESSAGES.waitingForSecretService}\n`);
|
|
10601
|
+
if (provider?.isKWallet) deps.stderr(`${kwalletUnlockHint(provider.walletLabel)}\n`);
|
|
10602
|
+
}
|
|
10155
10603
|
}).catch(() => stop$1("keychainUnavailable")) ?? stopWith(keychainUnavailableMessage(deps.keychain));
|
|
10156
10604
|
}
|
|
10157
10605
|
function revokeHashes({ items }) {
|
|
@@ -13706,7 +14154,12 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
13706
14154
|
load: () => import("@napi-rs/keyring"),
|
|
13707
14155
|
secretService: createSecretServiceProcess({
|
|
13708
14156
|
execPath: process.execPath,
|
|
13709
|
-
resolveKeyring: () => require.resolve("@napi-rs/keyring")
|
|
14157
|
+
resolveKeyring: () => require.resolve("@napi-rs/keyring"),
|
|
14158
|
+
lock: createSecretServiceLock({
|
|
14159
|
+
address: sessionBusAddress(process.env),
|
|
14160
|
+
uid: process.getuid?.() ?? 0,
|
|
14161
|
+
readComm: (pid) => readFile(`/proc/${pid}/comm`, "utf8")
|
|
14162
|
+
})
|
|
13710
14163
|
})
|
|
13711
14164
|
}),
|
|
13712
14165
|
stdout: (text) => process.stdout.write(text),
|