@jitsusama/agentic-harness.core 0.6.0 → 0.6.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.
@@ -42,11 +42,14 @@ export interface HookInstall {
42
42
  export declare function installCommitHook(repoRoot: string, options: CommitHookOptions): HookInstall;
43
43
  /**
44
44
  * Ensure the hook is installed in the repo containing dir, at most
45
- * once per repo root. Resolves the repo, records it in `installed`
46
- * so later commands in the same repo are skipped, and installs
47
- * best-effort. A directory outside any git repo is a no-op. This is
48
- * how hook coverage follows the session into repos it later cds
49
- * into, rather than only the repo the session started in.
45
+ * once per repo. Records both dir and its repo root in `installed`,
46
+ * so a later command from either asks git nothing, and installs
47
+ * best-effort. A directory outside any git repo is a no-op and is
48
+ * not remembered, so a repo initialised there later is still
49
+ * covered. This is how hook coverage follows the session into repos
50
+ * it later cds into, rather than only the repo the session started
51
+ * in. Each git call is a synchronous spawn on the command path, so
52
+ * a first visit costs one and a repeat costs none.
50
53
  */
51
54
  export declare function ensureCommitHook(dir: string, installed: Set<string>, options: CommitHookOptions): void;
52
55
  /** The git repository root containing dir, or null when there is none. */
@@ -46,20 +46,20 @@ git interpret-trailers --in-place --trailer ${options.trailerExpr} "$msg_file"
46
46
  * hook. A no-op when this adapter's hook is already installed.
47
47
  */
48
48
  export function installCommitHook(repoRoot, options) {
49
+ const layout = locateHooks(repoRoot);
50
+ if (!layout)
51
+ return { installed: false, reason: "not a git repo" };
52
+ return installInto(layout, options);
53
+ }
54
+ /** Install the hook into a repo whose hooks have been located. */
55
+ function installInto({ hooksDir, customHooksPath }, options) {
49
56
  // A custom core.hooksPath means a hook manager (husky and the
50
57
  // like) or a shared, possibly version-controlled hooks directory
51
58
  // owns the hooks. Leave it alone rather than write this adapter's
52
59
  // hook into a directory it does not own.
53
- if (hasCustomHooksPath(repoRoot)) {
60
+ if (customHooksPath) {
54
61
  return { installed: false, reason: "custom core.hooksPath configured" };
55
62
  }
56
- let hooksDir;
57
- try {
58
- hooksDir = resolveHooksDir(repoRoot);
59
- }
60
- catch (error) {
61
- return { installed: false, reason: `not a git repo: ${String(error)}` };
62
- }
63
63
  const target = join(hooksDir, "prepare-commit-msg");
64
64
  if (existsSync(target) &&
65
65
  readFileSync(target, "utf8").includes(options.marker)) {
@@ -82,41 +82,66 @@ export function installCommitHook(repoRoot, options) {
82
82
  chmodSync(target, 0o755);
83
83
  return { installed: true };
84
84
  }
85
- /** Whether the repo configures a custom core.hooksPath. */
86
- function hasCustomHooksPath(repoRoot) {
87
- try {
88
- const value = execFileSync("git", ["-C", repoRoot, "config", "--get", "core.hooksPath"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
89
- return value.length > 0;
90
- }
91
- catch {
92
- // git config exits non-zero when the key is unset: no custom path.
93
- return false;
94
- }
95
- }
96
85
  /**
97
86
  * Ensure the hook is installed in the repo containing dir, at most
98
- * once per repo root. Resolves the repo, records it in `installed`
99
- * so later commands in the same repo are skipped, and installs
100
- * best-effort. A directory outside any git repo is a no-op. This is
101
- * how hook coverage follows the session into repos it later cds
102
- * into, rather than only the repo the session started in.
87
+ * once per repo. Records both dir and its repo root in `installed`,
88
+ * so a later command from either asks git nothing, and installs
89
+ * best-effort. A directory outside any git repo is a no-op and is
90
+ * not remembered, so a repo initialised there later is still
91
+ * covered. This is how hook coverage follows the session into repos
92
+ * it later cds into, rather than only the repo the session started
93
+ * in. Each git call is a synchronous spawn on the command path, so
94
+ * a first visit costs one and a repeat costs none.
103
95
  */
104
96
  export function ensureCommitHook(dir, installed, options) {
105
- const root = repoRootOf(dir);
106
- if (!root || installed.has(root))
97
+ if (installed.has(dir))
98
+ return;
99
+ const layout = locateHooks(dir);
100
+ if (!layout)
107
101
  return;
108
- installed.add(root);
102
+ installed.add(dir);
103
+ if (installed.has(layout.root))
104
+ return;
105
+ installed.add(layout.root);
109
106
  try {
110
- installCommitHook(root, options);
107
+ installInto(layout, options);
111
108
  }
112
109
  catch {
113
110
  // Best-effort: never let hook installation break a command.
114
111
  }
115
112
  }
116
- /** Resolve the active hooks directory, honouring core.hooksPath. */
117
- function resolveHooksDir(repoRoot) {
118
- const path = execFileSync("git", ["-C", repoRoot, "rev-parse", "--git-path", "hooks"], { encoding: "utf8" }).trim();
119
- return isAbsolute(path) ? path : join(repoRoot, path);
113
+ /**
114
+ * Locate the hooks of the repo containing dir with one git call, or
115
+ * null when dir is in no working tree. Git prints the default hooks
116
+ * path as the common dir plus `/hooks`, in the same form, so any
117
+ * other answer means core.hooksPath points somewhere else.
118
+ */
119
+ function locateHooks(dir) {
120
+ let answer;
121
+ try {
122
+ answer = execFileSync("git", [
123
+ "-C",
124
+ dir,
125
+ "rev-parse",
126
+ "--show-toplevel",
127
+ "--git-common-dir",
128
+ "--git-path",
129
+ "hooks",
130
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
131
+ }
132
+ catch {
133
+ // Not in a working tree (or git unavailable): nothing to hook.
134
+ return null;
135
+ }
136
+ const [root, commonDir, hooks] = answer.trimEnd().split("\n");
137
+ if (!root || !commonDir || !hooks)
138
+ return null;
139
+ return {
140
+ root,
141
+ // Relative paths are relative to the directory git ran in.
142
+ hooksDir: isAbsolute(hooks) ? hooks : join(dir, hooks),
143
+ customHooksPath: hooks !== `${commonDir}/hooks`,
144
+ };
120
145
  }
121
146
  /** The git repository root containing dir, or null when there is none. */
122
147
  export function repoRootOf(dir) {
@@ -79,6 +79,11 @@ export declare function identityFromInspection(hostId: string, pid: number, insp
79
79
  * read a real start token: a session then carries no process identity
80
80
  * and its liveness falls back to recency, rather than a synthetic
81
81
  * token that a later probe would read as a dead mismatch.
82
+ *
83
+ * A process's own start token cannot change while it runs, so the
84
+ * first real reading is remembered: a session start attaches more
85
+ * than once and each reading is a synchronous `ps`. A failed reading
86
+ * is not remembered, so a transient failure is retried next time.
82
87
  */
83
88
  export declare function currentProcessIdentity(): ProcessIdentity | undefined;
84
89
  /**
@@ -95,14 +95,26 @@ export function identityFromInspection(hostId, pid, inspection) {
95
95
  ? { hostId, pid, startToken: inspection.startToken }
96
96
  : undefined;
97
97
  }
98
+ /** Memoized identity of this process, once one has been read. */
99
+ let processIdentityCache;
98
100
  /**
99
101
  * Identity of the currently running pi process, for capture onto the
100
102
  * session it is attached to. Undefined when the OS reader could not
101
103
  * read a real start token: a session then carries no process identity
102
104
  * and its liveness falls back to recency, rather than a synthetic
103
105
  * token that a later probe would read as a dead mismatch.
106
+ *
107
+ * A process's own start token cannot change while it runs, so the
108
+ * first real reading is remembered: a session start attaches more
109
+ * than once and each reading is a synchronous `ps`. A failed reading
110
+ * is not remembered, so a transient failure is retried next time.
104
111
  */
105
112
  export function currentProcessIdentity() {
113
+ processIdentityCache ??= readProcessIdentity();
114
+ return processIdentityCache;
115
+ }
116
+ /** Read this process's identity from the OS. */
117
+ function readProcessIdentity() {
106
118
  const identity = identityFromInspection(hostname(), process.pid, readStartToken(process.pid));
107
119
  if (!identity)
108
120
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jitsusama/agentic-harness.core",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Pi-agnostic business logic for agentic-harness: state machines, guardian decisions, quest/TDD domain model.",
5
5
  "license": "MIT",
6
6
  "type": "module",