@made-by-moonlight/athene-core 0.9.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/LICENSE +22 -0
- package/README.md +241 -0
- package/dist/activity-events.d.ts +42 -0
- package/dist/activity-events.d.ts.map +1 -0
- package/dist/activity-events.js +192 -0
- package/dist/activity-events.js.map +1 -0
- package/dist/activity-log.d.ts +71 -0
- package/dist/activity-log.d.ts.map +1 -0
- package/dist/activity-log.js +203 -0
- package/dist/activity-log.js.map +1 -0
- package/dist/activity-signal.d.ts +20 -0
- package/dist/activity-signal.d.ts.map +1 -0
- package/dist/activity-signal.js +91 -0
- package/dist/activity-signal.js.map +1 -0
- package/dist/agent-report.d.ts +148 -0
- package/dist/agent-report.d.ts.map +1 -0
- package/dist/agent-report.js +516 -0
- package/dist/agent-report.js.map +1 -0
- package/dist/agent-selection.d.ts +31 -0
- package/dist/agent-selection.d.ts.map +1 -0
- package/dist/agent-selection.js +69 -0
- package/dist/agent-selection.js.map +1 -0
- package/dist/agent-workspace-hooks.d.ts +74 -0
- package/dist/agent-workspace-hooks.d.ts.map +1 -0
- package/dist/agent-workspace-hooks.js +988 -0
- package/dist/agent-workspace-hooks.js.map +1 -0
- package/dist/atomic-write.d.ts +6 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +49 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/cleanup-stack.d.ts +37 -0
- package/dist/cleanup-stack.d.ts.map +1 -0
- package/dist/cleanup-stack.js +45 -0
- package/dist/cleanup-stack.js.map +1 -0
- package/dist/code-review-manager.d.ts +118 -0
- package/dist/code-review-manager.d.ts.map +1 -0
- package/dist/code-review-manager.js +719 -0
- package/dist/code-review-manager.js.map +1 -0
- package/dist/code-review-store.d.ts +114 -0
- package/dist/code-review-store.d.ts.map +1 -0
- package/dist/code-review-store.js +346 -0
- package/dist/code-review-store.js.map +1 -0
- package/dist/config-generator.d.ts +84 -0
- package/dist/config-generator.d.ts.map +1 -0
- package/dist/config-generator.js +295 -0
- package/dist/config-generator.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +852 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon-children.d.ts +55 -0
- package/dist/daemon-children.d.ts.map +1 -0
- package/dist/daemon-children.js +435 -0
- package/dist/daemon-children.js.map +1 -0
- package/dist/dashboard-notifications.d.ts +42 -0
- package/dist/dashboard-notifications.d.ts.map +1 -0
- package/dist/dashboard-notifications.js +123 -0
- package/dist/dashboard-notifications.js.map +1 -0
- package/dist/events-db.d.ts +39 -0
- package/dist/events-db.d.ts.map +1 -0
- package/dist/events-db.js +185 -0
- package/dist/events-db.js.map +1 -0
- package/dist/feature-flags.d.ts +2 -0
- package/dist/feature-flags.d.ts.map +1 -0
- package/dist/feature-flags.js +9 -0
- package/dist/feature-flags.js.map +1 -0
- package/dist/feedback-tools.d.ts +97 -0
- package/dist/feedback-tools.d.ts.map +1 -0
- package/dist/feedback-tools.js +161 -0
- package/dist/feedback-tools.js.map +1 -0
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +59 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/format-automated-comments.d.ts +18 -0
- package/dist/format-automated-comments.d.ts.map +1 -0
- package/dist/gh-trace.d.ts +57 -0
- package/dist/gh-trace.d.ts.map +1 -0
- package/dist/gh-trace.js +320 -0
- package/dist/gh-trace.js.map +1 -0
- package/dist/git-activity.d.ts +10 -0
- package/dist/git-activity.d.ts.map +1 -0
- package/dist/git-activity.js +30 -0
- package/dist/git-activity.js.map +1 -0
- package/dist/global-config.d.ts +1085 -0
- package/dist/global-config.d.ts.map +1 -0
- package/dist/global-config.js +1067 -0
- package/dist/global-config.js.map +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/key-value.d.ts +7 -0
- package/dist/key-value.d.ts.map +1 -0
- package/dist/key-value.js +24 -0
- package/dist/key-value.js.map +1 -0
- package/dist/lifecycle-manager.d.ts +22 -0
- package/dist/lifecycle-manager.d.ts.map +1 -0
- package/dist/lifecycle-manager.js +2813 -0
- package/dist/lifecycle-manager.js.map +1 -0
- package/dist/lifecycle-state.d.ts +28 -0
- package/dist/lifecycle-state.d.ts.map +1 -0
- package/dist/lifecycle-state.js +446 -0
- package/dist/lifecycle-state.js.map +1 -0
- package/dist/lifecycle-status-decisions.d.ts +85 -0
- package/dist/lifecycle-status-decisions.d.ts.map +1 -0
- package/dist/lifecycle-status-decisions.js +262 -0
- package/dist/lifecycle-status-decisions.js.map +1 -0
- package/dist/lifecycle-transition.d.ts +81 -0
- package/dist/lifecycle-transition.d.ts.map +1 -0
- package/dist/lifecycle-transition.js +207 -0
- package/dist/lifecycle-transition.js.map +1 -0
- package/dist/metadata.d.ts +54 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +484 -0
- package/dist/metadata.js.map +1 -0
- package/dist/migration/storage-v2.d.ts +76 -0
- package/dist/migration/storage-v2.d.ts.map +1 -0
- package/dist/migration/storage-v2.js +1614 -0
- package/dist/migration/storage-v2.js.map +1 -0
- package/dist/notification-data.d.ts +135 -0
- package/dist/notification-data.d.ts.map +1 -0
- package/dist/notification-data.js +204 -0
- package/dist/notification-data.js.map +1 -0
- package/dist/notification-observability.d.ts +21 -0
- package/dist/notification-observability.d.ts.map +1 -0
- package/dist/notification-observability.js +154 -0
- package/dist/notification-observability.js.map +1 -0
- package/dist/notifier-resolution.d.ts +14 -0
- package/dist/notifier-resolution.d.ts.map +1 -0
- package/dist/notifier-resolution.js +23 -0
- package/dist/notifier-resolution.js.map +1 -0
- package/dist/observability.d.ts +100 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +535 -0
- package/dist/observability.js.map +1 -0
- package/dist/opencode-agents-md.d.ts +3 -0
- package/dist/opencode-agents-md.d.ts.map +1 -0
- package/dist/opencode-agents-md.js +40 -0
- package/dist/opencode-agents-md.js.map +1 -0
- package/dist/opencode-config.d.ts +2 -0
- package/dist/opencode-config.d.ts.map +1 -0
- package/dist/opencode-config.js +17 -0
- package/dist/opencode-config.js.map +1 -0
- package/dist/opencode-session-id.d.ts +2 -0
- package/dist/opencode-session-id.d.ts.map +1 -0
- package/dist/opencode-session-id.js +12 -0
- package/dist/opencode-session-id.js.map +1 -0
- package/dist/opencode-shared.d.ts +80 -0
- package/dist/opencode-shared.d.ts.map +1 -0
- package/dist/opencode-shared.js +202 -0
- package/dist/opencode-shared.js.map +1 -0
- package/dist/orchestrator-prompt.d.ts +19 -0
- package/dist/orchestrator-prompt.d.ts.map +1 -0
- package/dist/orchestrator-prompt.js +130 -0
- package/dist/orchestrator-prompt.js.map +1 -0
- package/dist/orchestrator-session-strategy.d.ts +5 -0
- package/dist/orchestrator-session-strategy.d.ts.map +1 -0
- package/dist/orchestrator-session-strategy.js +13 -0
- package/dist/orchestrator-session-strategy.js.map +1 -0
- package/dist/paths.d.ts +145 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +288 -0
- package/dist/paths.js.map +1 -0
- package/dist/platform.d.ts +32 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +211 -0
- package/dist/platform.js.map +1 -0
- package/dist/plugin-registry.d.ts +15 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +499 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/portfolio-projects.d.ts +7 -0
- package/dist/portfolio-projects.d.ts.map +1 -0
- package/dist/portfolio-projects.js +65 -0
- package/dist/portfolio-projects.js.map +1 -0
- package/dist/portfolio-registry.d.ts +42 -0
- package/dist/portfolio-registry.d.ts.map +1 -0
- package/dist/portfolio-registry.js +311 -0
- package/dist/portfolio-registry.js.map +1 -0
- package/dist/portfolio-routing.d.ts +5 -0
- package/dist/portfolio-routing.d.ts.map +1 -0
- package/dist/portfolio-routing.js +24 -0
- package/dist/portfolio-routing.js.map +1 -0
- package/dist/portfolio-session-service.d.ts +15 -0
- package/dist/portfolio-session-service.d.ts.map +1 -0
- package/dist/portfolio-session-service.js +206 -0
- package/dist/portfolio-session-service.js.map +1 -0
- package/dist/process-cache.d.ts +32 -0
- package/dist/process-cache.d.ts.map +1 -0
- package/dist/process-cache.js +44 -0
- package/dist/process-cache.js.map +1 -0
- package/dist/project-resolver.d.ts +5 -0
- package/dist/project-resolver.d.ts.map +1 -0
- package/dist/project-resolver.js +20 -0
- package/dist/project-resolver.js.map +1 -0
- package/dist/prompt-builder.d.ts +42 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +182 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/prompts/orchestrator.md.js +4 -0
- package/dist/prompts/orchestrator.md.js.map +1 -0
- package/dist/query-activity-events.d.ts +42 -0
- package/dist/query-activity-events.d.ts.map +1 -0
- package/dist/query-activity-events.js +170 -0
- package/dist/query-activity-events.js.map +1 -0
- package/dist/recovery/actions.d.ts +7 -0
- package/dist/recovery/actions.d.ts.map +1 -0
- package/dist/recovery/index.d.ts +8 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/logger.d.ts +12 -0
- package/dist/recovery/logger.d.ts.map +1 -0
- package/dist/recovery/manager.d.ts +24 -0
- package/dist/recovery/manager.d.ts.map +1 -0
- package/dist/recovery/scanner.d.ts +11 -0
- package/dist/recovery/scanner.d.ts.map +1 -0
- package/dist/recovery/types.d.ts +170 -0
- package/dist/recovery/types.d.ts.map +1 -0
- package/dist/recovery/validator.d.ts +8 -0
- package/dist/recovery/validator.d.ts.map +1 -0
- package/dist/report-watcher.d.ts +93 -0
- package/dist/report-watcher.d.ts.map +1 -0
- package/dist/report-watcher.js +182 -0
- package/dist/report-watcher.js.map +1 -0
- package/dist/scm-webhook-utils.d.ts +6 -0
- package/dist/scm-webhook-utils.d.ts.map +1 -0
- package/dist/scm-webhook-utils.js +36 -0
- package/dist/scm-webhook-utils.js.map +1 -0
- package/dist/session-manager.d.ts +22 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +3077 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/spawn-target.d.ts +23 -0
- package/dist/spawn-target.d.ts.map +1 -0
- package/dist/spawn-target.js +39 -0
- package/dist/spawn-target.js.map +1 -0
- package/dist/storage-key.d.ts +9 -0
- package/dist/storage-key.d.ts.map +1 -0
- package/dist/storage-key.js +59 -0
- package/dist/storage-key.js.map +1 -0
- package/dist/tmux.d.ts +39 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +141 -0
- package/dist/tmux.js.map +1 -0
- package/dist/types.d.ts +1496 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +215 -0
- package/dist/types.js.map +1 -0
- package/dist/update-cache.d.ts +59 -0
- package/dist/update-cache.d.ts.map +1 -0
- package/dist/update-cache.js +77 -0
- package/dist/update-cache.js.map +1 -0
- package/dist/utils/metadata-flatten.d.ts +3 -0
- package/dist/utils/metadata-flatten.d.ts.map +1 -0
- package/dist/utils/metadata-flatten.js +18 -0
- package/dist/utils/metadata-flatten.js.map +1 -0
- package/dist/utils/pr.d.ts +7 -0
- package/dist/utils/pr.d.ts.map +1 -0
- package/dist/utils/pr.js +97 -0
- package/dist/utils/pr.js.map +1 -0
- package/dist/utils/session-from-metadata.d.ts +16 -0
- package/dist/utils/session-from-metadata.d.ts.map +1 -0
- package/dist/utils/session-from-metadata.js +87 -0
- package/dist/utils/session-from-metadata.js.map +1 -0
- package/dist/utils/session-id.d.ts +4 -0
- package/dist/utils/session-id.d.ts.map +1 -0
- package/dist/utils/session-id.js +9 -0
- package/dist/utils/session-id.js.map +1 -0
- package/dist/utils/validation.d.ts +9 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +45 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils.d.ts +65 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +189 -0
- package/dist/utils.js.map +1 -0
- package/dist/version-compare.d.ts +27 -0
- package/dist/version-compare.d.ts.map +1 -0
- package/dist/version-compare.js +121 -0
- package/dist/version-compare.js.map +1 -0
- package/dist/windows-pty-registry.d.ts +27 -0
- package/dist/windows-pty-registry.d.ts.map +1 -0
- package/dist/windows-pty-registry.js +109 -0
- package/dist/windows-pty-registry.js.map +1 -0
- package/package.json +110 -0
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { isWindows } from './platform.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared PATH-based workspace hooks for all agent plugins.
|
|
9
|
+
*
|
|
10
|
+
* Installs ~/.ao/bin/gh and ~/.ao/bin/git wrappers that:
|
|
11
|
+
* - Intercept PR creation and branch operations to auto-update session metadata
|
|
12
|
+
* - Cache repeated read-only gh commands (PR discovery, issue context) to reduce
|
|
13
|
+
* GitHub API traffic — see D4-wrapper-cache-plan.md for design
|
|
14
|
+
*
|
|
15
|
+
* The session manager injects these wrappers into every agent's PATH,
|
|
16
|
+
* including Claude Code (which also has its own PostToolUse hooks for writes).
|
|
17
|
+
*/
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Constants
|
|
20
|
+
// =============================================================================
|
|
21
|
+
const DEFAULT_PATH = "/usr/bin:/bin";
|
|
22
|
+
const PREFERRED_GH_BIN_DIR = "/usr/local/bin";
|
|
23
|
+
/** Preferred gh binary path for wrapper scripts */
|
|
24
|
+
const PREFERRED_GH_PATH = `${PREFERRED_GH_BIN_DIR}/gh`;
|
|
25
|
+
/**
|
|
26
|
+
* Get the shared bin directory for ao shell wrappers (prepended to PATH).
|
|
27
|
+
* Computed lazily to avoid calling homedir() at module load time,
|
|
28
|
+
* which breaks test mocks that replace homedir after import.
|
|
29
|
+
*/
|
|
30
|
+
function getAoBinDir() {
|
|
31
|
+
return join(homedir(), ".ao", "bin");
|
|
32
|
+
}
|
|
33
|
+
/** Current version of wrapper scripts — bump when scripts change */
|
|
34
|
+
const WRAPPER_VERSION = "0.8.0";
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// PATH Builder
|
|
37
|
+
// =============================================================================
|
|
38
|
+
/**
|
|
39
|
+
* Build a PATH string with ~/.ao/bin prepended for wrapper interception.
|
|
40
|
+
* Deduplicates entries and ensures /usr/local/bin is early for gh resolution.
|
|
41
|
+
*/
|
|
42
|
+
function buildAgentPath(basePath) {
|
|
43
|
+
const delimiter = isWindows() ? ";" : ":";
|
|
44
|
+
const inherited = (basePath ?? (isWindows() ? "" : DEFAULT_PATH))
|
|
45
|
+
.split(delimiter)
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
const ordered = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const add = (entry) => {
|
|
50
|
+
if (!entry || seen.has(entry))
|
|
51
|
+
return;
|
|
52
|
+
ordered.push(entry);
|
|
53
|
+
seen.add(entry);
|
|
54
|
+
};
|
|
55
|
+
add(getAoBinDir());
|
|
56
|
+
if (!isWindows()) {
|
|
57
|
+
add(PREFERRED_GH_BIN_DIR);
|
|
58
|
+
}
|
|
59
|
+
for (const entry of inherited)
|
|
60
|
+
add(entry);
|
|
61
|
+
return ordered.join(delimiter);
|
|
62
|
+
}
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Shell Wrapper Scripts
|
|
65
|
+
// =============================================================================
|
|
66
|
+
/* eslint-disable no-useless-escape -- \$ escapes are intentional: bash scripts in JS template literals */
|
|
67
|
+
/**
|
|
68
|
+
* Helper script sourced by both gh and git wrappers.
|
|
69
|
+
* Provides:
|
|
70
|
+
* update_ao_metadata <key> <value> — write key=value to session metadata
|
|
71
|
+
* read_ao_metadata <key> — read a value from session metadata
|
|
72
|
+
* ao_cache_dir — print the per-session gh cache directory
|
|
73
|
+
* ao_cache_fresh <key> <max_age> — test if a cache entry is fresh (0 = infinite)
|
|
74
|
+
* ao_cache_read <key> — print cached stdout
|
|
75
|
+
* ao_cache_write <key> — write stdin to cache atomically
|
|
76
|
+
*/
|
|
77
|
+
const AO_METADATA_HELPER = `#!/usr/bin/env bash
|
|
78
|
+
# ao-metadata-helper — shared by gh/git wrappers
|
|
79
|
+
# Provides: update_ao_metadata, read_ao_metadata, ao_cache_*
|
|
80
|
+
|
|
81
|
+
# ── Shared validation ────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
_ao_validate_env() {
|
|
84
|
+
local ao_dir="\${AO_DATA_DIR:-}"
|
|
85
|
+
local ao_session="\${AO_SESSION:-}"
|
|
86
|
+
[[ -z "\$ao_dir" || -z "\$ao_session" ]] && return 1
|
|
87
|
+
case "\$ao_session" in */* | *..*) return 1 ;; esac
|
|
88
|
+
case "\$ao_dir" in
|
|
89
|
+
"\$HOME"/.ao/* | "\$HOME"/.agent-orchestrator/* | /tmp/*) ;;
|
|
90
|
+
*) return 1 ;;
|
|
91
|
+
esac
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── Metadata write ───────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
update_ao_metadata() {
|
|
98
|
+
local key="\$1" value="\$2"
|
|
99
|
+
local ao_dir="\${AO_DATA_DIR:-}"
|
|
100
|
+
local ao_session="\${AO_SESSION:-}"
|
|
101
|
+
|
|
102
|
+
[[ -z "\$ao_dir" || -z "\$ao_session" ]] && return 0
|
|
103
|
+
|
|
104
|
+
# Validate: session name must not contain path separators or traversal
|
|
105
|
+
case "\$ao_session" in
|
|
106
|
+
*/* | *..*) return 0 ;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
# Validate: ao_dir must be an absolute path under known ao directories or /tmp
|
|
110
|
+
case "\$ao_dir" in
|
|
111
|
+
"\$HOME"/.ao/* | "\$HOME"/.agent-orchestrator/* | /tmp/*) ;;
|
|
112
|
+
*) return 0 ;;
|
|
113
|
+
esac
|
|
114
|
+
|
|
115
|
+
# V2 storage uses .json extension; fallback to bare filename for pre-migration layouts
|
|
116
|
+
local metadata_file="\$ao_dir/\${ao_session}.json"
|
|
117
|
+
if [[ ! -f "\$metadata_file" ]]; then
|
|
118
|
+
metadata_file="\$ao_dir/\$ao_session"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Resolve symlinks and verify canonicalized paths are still within trusted roots
|
|
122
|
+
local real_dir real_ao_dir
|
|
123
|
+
real_ao_dir="\$(cd "\$ao_dir" 2>/dev/null && pwd -P)" || return 0
|
|
124
|
+
real_dir="\$(cd "\$(dirname "\$metadata_file")" 2>/dev/null && pwd -P)" || return 0
|
|
125
|
+
|
|
126
|
+
# Re-validate real_ao_dir against trusted roots after canonicalization
|
|
127
|
+
# (prevents /tmp/../../home/user from escaping the allowlist)
|
|
128
|
+
case "\$real_ao_dir" in
|
|
129
|
+
"\$HOME"/.ao/* | "\$HOME"/.ao | "\$HOME"/.agent-orchestrator/* | "\$HOME"/.agent-orchestrator | /tmp/*) ;;
|
|
130
|
+
*) return 0 ;;
|
|
131
|
+
esac
|
|
132
|
+
|
|
133
|
+
[[ "\$real_dir" == "\$real_ao_dir"* ]] || return 0
|
|
134
|
+
|
|
135
|
+
[[ -f "\$metadata_file" ]] || return 0
|
|
136
|
+
|
|
137
|
+
# Validate key — only allow alphanumeric, underscore, hyphen (prevents sed/jq injection)
|
|
138
|
+
[[ "\$key" =~ ^[a-zA-Z0-9_-]+$ ]] || return 0
|
|
139
|
+
|
|
140
|
+
local temp_file="\${metadata_file}.tmp.\$\$"
|
|
141
|
+
|
|
142
|
+
# Strip newlines from value to prevent injection
|
|
143
|
+
local clean_value="\$(printf '%s' "\$value" | tr -d '\\n')"
|
|
144
|
+
|
|
145
|
+
# Detect JSON vs key=value format
|
|
146
|
+
local first_char
|
|
147
|
+
first_char="\$(head -c1 "\$metadata_file" 2>/dev/null)"
|
|
148
|
+
|
|
149
|
+
if [[ "\$first_char" == "{" ]]; then
|
|
150
|
+
# JSON format
|
|
151
|
+
if command -v jq &>/dev/null; then
|
|
152
|
+
jq --arg k "\$key" --arg v "\$clean_value" '.[\$k] = \$v' "\$metadata_file" > "\$temp_file"
|
|
153
|
+
mv "\$temp_file" "\$metadata_file"
|
|
154
|
+
else
|
|
155
|
+
# jq unavailable — use node (hard dep) for safe nested JSON update
|
|
156
|
+
node -e "
|
|
157
|
+
const fs = require('fs');
|
|
158
|
+
const d = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
159
|
+
d[process.argv[2]] = process.argv[3];
|
|
160
|
+
fs.writeFileSync(process.argv[4], JSON.stringify(d, null, 2));
|
|
161
|
+
" "\$metadata_file" "\$key" "\$clean_value" "\$temp_file"
|
|
162
|
+
mv "\$temp_file" "\$metadata_file"
|
|
163
|
+
fi
|
|
164
|
+
else
|
|
165
|
+
# Key=value format (legacy)
|
|
166
|
+
local escaped_value="\$(printf '%s' "\$clean_value" | sed 's/[&|\\\\]/\\\\&/g')"
|
|
167
|
+
if grep -q "^\${key}=" "\$metadata_file" 2>/dev/null; then
|
|
168
|
+
sed "s|^\${key}=.*|\${key}=\${escaped_value}|" "\$metadata_file" > "\$temp_file"
|
|
169
|
+
else
|
|
170
|
+
cp "\$metadata_file" "\$temp_file"
|
|
171
|
+
printf '%s=%s\\n' "\$key" "\$clean_value" >> "\$temp_file"
|
|
172
|
+
fi
|
|
173
|
+
mv "\$temp_file" "\$metadata_file"
|
|
174
|
+
fi
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ── Metadata read ────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
read_ao_metadata() {
|
|
180
|
+
local key="\$1"
|
|
181
|
+
_ao_validate_env || return 1
|
|
182
|
+
local metadata_file="\${AO_DATA_DIR}/\${AO_SESSION}"
|
|
183
|
+
[[ -f "\$metadata_file" ]] || return 1
|
|
184
|
+
[[ "\$key" =~ ^[a-zA-Z0-9_-]+$ ]] || return 1
|
|
185
|
+
local line
|
|
186
|
+
line=\$(grep "^\${key}=" "\$metadata_file" 2>/dev/null | head -1) || return 1
|
|
187
|
+
printf '%s' "\${line#*=}"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# ── Cache helpers ────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
ao_cache_dir() {
|
|
193
|
+
_ao_validate_env || return 1
|
|
194
|
+
local d="\${AO_DATA_DIR}/.ghcache/\${AO_SESSION}"
|
|
195
|
+
mkdir -p "\$d" 2>/dev/null || return 1
|
|
196
|
+
printf '%s' "\$d"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
ao_cache_fresh() {
|
|
200
|
+
local cache_key="\$1" max_age="\$2"
|
|
201
|
+
[[ "\$cache_key" =~ ^[a-zA-Z0-9_.-]+$ ]] || return 1
|
|
202
|
+
local cache_dir
|
|
203
|
+
cache_dir="\$(ao_cache_dir)" || return 1
|
|
204
|
+
local ts_file="\$cache_dir/\${cache_key}.ts"
|
|
205
|
+
local stdout_file="\$cache_dir/\${cache_key}.stdout"
|
|
206
|
+
[[ -f "\$stdout_file" && -f "\$ts_file" ]] || return 1
|
|
207
|
+
local cached_ts now
|
|
208
|
+
cached_ts=\$(cat "\$ts_file" 2>/dev/null) || return 1
|
|
209
|
+
# Sanity check: cached_ts must be a positive integer (epoch seconds)
|
|
210
|
+
[[ "\$cached_ts" =~ ^[0-9]+$ && "\$cached_ts" -gt 0 ]] || return 1
|
|
211
|
+
# max_age=0 means infinite TTL
|
|
212
|
+
[[ "\$max_age" -eq 0 ]] 2>/dev/null && return 0
|
|
213
|
+
now=\$(date +%s)
|
|
214
|
+
(( now - cached_ts < max_age ))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
ao_cache_read() {
|
|
218
|
+
local cache_key="\$1"
|
|
219
|
+
[[ "\$cache_key" =~ ^[a-zA-Z0-9_.-]+$ ]] || return 1
|
|
220
|
+
local cache_dir
|
|
221
|
+
cache_dir="\$(ao_cache_dir)" || return 1
|
|
222
|
+
cat "\$cache_dir/\${cache_key}.stdout"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
ao_cache_write() {
|
|
226
|
+
local cache_key="\$1"
|
|
227
|
+
[[ "\$cache_key" =~ ^[a-zA-Z0-9_.-]+$ ]] || return 1
|
|
228
|
+
local cache_dir
|
|
229
|
+
cache_dir="\$(ao_cache_dir)" || return 1
|
|
230
|
+
local tmp="\$cache_dir/\${cache_key}.stdout.tmp.\$\$"
|
|
231
|
+
cat > "\$tmp" && mv "\$tmp" "\$cache_dir/\${cache_key}.stdout"
|
|
232
|
+
date +%s > "\$cache_dir/\${cache_key}.ts"
|
|
233
|
+
}
|
|
234
|
+
`;
|
|
235
|
+
/**
|
|
236
|
+
* gh wrapper — intercepts agent-side gh calls for:
|
|
237
|
+
* 1. Caching repeated read-only commands (PR discovery, issue context)
|
|
238
|
+
* 2. Auto-updating session metadata on PR creation
|
|
239
|
+
*
|
|
240
|
+
* Cache storage: $AO_DATA_DIR/.ghcache/$AO_SESSION/{key}.stdout + {key}.ts
|
|
241
|
+
* See D4-wrapper-cache-plan.md for full design rationale.
|
|
242
|
+
*/
|
|
243
|
+
const GH_WRAPPER = `#!/usr/bin/env bash
|
|
244
|
+
# ao gh wrapper — caches reads + auto-updates metadata on writes
|
|
245
|
+
|
|
246
|
+
# Find real gh by removing our wrapper directory from PATH
|
|
247
|
+
ao_bin_dir="\$(cd "\$(dirname "\$0")" && pwd)"
|
|
248
|
+
clean_path="\$(echo "\$PATH" | tr ':' '\\n' | grep -Fxv "\$ao_bin_dir" | grep . | tr '\\n' ':')"
|
|
249
|
+
clean_path="\${clean_path%:}"
|
|
250
|
+
real_gh=""
|
|
251
|
+
|
|
252
|
+
# Prefer explicit gh path when provided by AO environment.
|
|
253
|
+
# Guard against recursive self-reference to the wrapper in ~/.ao/bin.
|
|
254
|
+
if [[ -n "\${GH_PATH:-}" && -x "\$GH_PATH" ]]; then
|
|
255
|
+
gh_dir="\$(cd "\$(dirname "\$GH_PATH")" 2>/dev/null && pwd)"
|
|
256
|
+
if [[ "\$gh_dir" != "\$ao_bin_dir" ]]; then
|
|
257
|
+
real_gh="\$GH_PATH"
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
if [[ -z "\$real_gh" ]]; then
|
|
262
|
+
real_gh="\$(PATH="\$clean_path" command -v gh 2>/dev/null)"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if [[ -z "\$real_gh" ]]; then
|
|
266
|
+
echo "ao-wrapper: gh not found in PATH" >&2
|
|
267
|
+
exit 127
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# Source the metadata helper (provides update/read_ao_metadata, ao_cache_*)
|
|
271
|
+
source "\$ao_bin_dir/ao-metadata-helper.sh" 2>/dev/null || true
|
|
272
|
+
|
|
273
|
+
# Redact sensitive values from args before tracing.
|
|
274
|
+
# Handles: -H "Authorization: ...", token=..., password=..., secret=...
|
|
275
|
+
_ao_redact_args() {
|
|
276
|
+
local prev=""
|
|
277
|
+
local out=()
|
|
278
|
+
for arg in "\$@"; do
|
|
279
|
+
if [[ "\$prev" == "-H" || "\$prev" == "--header" ]] && [[ "\$arg" =~ ^[Aa]uthorization: ]]; then
|
|
280
|
+
out+=("Authorization: [REDACTED]")
|
|
281
|
+
elif [[ "\$arg" =~ ^-H[Aa]uthorization: ]]; then
|
|
282
|
+
out+=("-HAuthorization: [REDACTED]")
|
|
283
|
+
elif [[ "\$arg" =~ ^[Tt]oken= ]]; then
|
|
284
|
+
out+=("token=[REDACTED]")
|
|
285
|
+
elif [[ "\$arg" =~ ^[Pp]assword= ]]; then
|
|
286
|
+
out+=("password=[REDACTED]")
|
|
287
|
+
elif [[ "\$arg" =~ ^[Ss]ecret= ]]; then
|
|
288
|
+
out+=("secret=[REDACTED]")
|
|
289
|
+
else
|
|
290
|
+
out+=("\$arg")
|
|
291
|
+
fi
|
|
292
|
+
prev="\$arg"
|
|
293
|
+
done
|
|
294
|
+
printf '%s\n' "\${out[@]}"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Best-effort JSONL tracing for agent-side gh invocations.
|
|
298
|
+
log_gh_invocation() {
|
|
299
|
+
local trace_file="\${AO_AGENT_GH_TRACE:-}"
|
|
300
|
+
[[ -z "\$trace_file" ]] && return 0
|
|
301
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
302
|
+
|
|
303
|
+
mkdir -p "\$(dirname "\$trace_file")" 2>/dev/null || return 0
|
|
304
|
+
|
|
305
|
+
local args_json
|
|
306
|
+
args_json="\$(_ao_redact_args "\$@" | jq -Rsc 'split("\n")[:-1]')" || return 0
|
|
307
|
+
|
|
308
|
+
# Compute operation: gh.{arg1}.{arg2} (mirrors AO-side extractOperation)
|
|
309
|
+
local _ao_op="gh"
|
|
310
|
+
[[ \$# -ge 1 ]] && _ao_op="gh.\$1"
|
|
311
|
+
[[ \$# -ge 2 && "\$2" != -* ]] && _ao_op="gh.\$1.\$2"
|
|
312
|
+
|
|
313
|
+
jq -nc \
|
|
314
|
+
--arg timestamp "\$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
315
|
+
--arg cwd "\$PWD" \
|
|
316
|
+
--arg operation "\$_ao_op" \
|
|
317
|
+
--arg aoSession "\${AO_SESSION:-}" \
|
|
318
|
+
--arg aoSessionName "\${AO_SESSION_NAME:-}" \
|
|
319
|
+
--arg aoProjectId "\${AO_PROJECT_ID:-}" \
|
|
320
|
+
--arg aoIssueId "\${AO_ISSUE_ID:-}" \
|
|
321
|
+
--arg aoCallerType "\${AO_CALLER_TYPE:-}" \
|
|
322
|
+
--arg pid "\$\$" \
|
|
323
|
+
--arg wrapperVersion "${WRAPPER_VERSION}" \
|
|
324
|
+
--argjson args "\$args_json" \
|
|
325
|
+
'{
|
|
326
|
+
timestamp: $timestamp,
|
|
327
|
+
cwd: $cwd,
|
|
328
|
+
args: $args,
|
|
329
|
+
operation: $operation,
|
|
330
|
+
aoSession: (if $aoSession == "" then null else $aoSession end),
|
|
331
|
+
aoSessionName: (if $aoSessionName == "" then null else $aoSessionName end),
|
|
332
|
+
aoProjectId: (if $aoProjectId == "" then null else $aoProjectId end),
|
|
333
|
+
aoIssueId: (if $aoIssueId == "" then null else $aoIssueId end),
|
|
334
|
+
aoCallerType: (if $aoCallerType == "" then null else $aoCallerType end),
|
|
335
|
+
pid: ($pid | tonumber),
|
|
336
|
+
wrapperVersion: $wrapperVersion
|
|
337
|
+
}' >> "\$trace_file" 2>/dev/null || true
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
log_gh_invocation "\$@"
|
|
341
|
+
|
|
342
|
+
# Best-effort cache-outcome tracing (appends to same JSONL trace file).
|
|
343
|
+
# result: hit | miss-stored | miss-write-failed | miss-negative | miss-error | passthrough
|
|
344
|
+
log_ao_cache() {
|
|
345
|
+
local result="\$1" cache_key="\$2" duration_ms="\${3:-0}" exit_code="\${4:-0}" ok="\${5:-true}"
|
|
346
|
+
local trace_file="\${AO_AGENT_GH_TRACE:-}"
|
|
347
|
+
[[ -z "\$trace_file" ]] && return 0
|
|
348
|
+
printf '{"timestamp":"%s","cacheResult":"%s","cacheKey":"%s","pid":%s,"durationMs":%s,"exitCode":%s,"ok":%s}\\n' \
|
|
349
|
+
"\$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "\$result" "\$cache_key" "\$\$" \
|
|
350
|
+
"\$duration_ms" "\$exit_code" "\$ok" \
|
|
351
|
+
>> "\$trace_file" 2>/dev/null || true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# =============================================================================
|
|
355
|
+
# Cacheable reads
|
|
356
|
+
# =============================================================================
|
|
357
|
+
|
|
358
|
+
# ── 1. PR discovery: gh pr list --head <B> --limit 1 ────────────────────────
|
|
359
|
+
# 120s TTL for positive results (non-empty array). Never caches [].
|
|
360
|
+
if [[ "\$1" == "pr" && "\$2" == "list" ]]; then
|
|
361
|
+
_ao_head="" _ao_limit="" _ao_json="" _ao_repo="" _ao_cacheable=true
|
|
362
|
+
_ao_saved_args=("\$@")
|
|
363
|
+
shift 2
|
|
364
|
+
while [[ \$# -gt 0 ]]; do
|
|
365
|
+
case "\$1" in
|
|
366
|
+
--head) _ao_head="\$2"; shift 2 ;;
|
|
367
|
+
--head=*) _ao_head="\${1#--head=}"; shift ;;
|
|
368
|
+
--limit) _ao_limit="\$2"; shift 2 ;;
|
|
369
|
+
--limit=*) _ao_limit="\${1#--limit=}"; shift ;;
|
|
370
|
+
--json) _ao_json="\$2"; shift 2 ;;
|
|
371
|
+
--json=*) _ao_json="\${1#--json=}"; shift ;;
|
|
372
|
+
--repo) _ao_repo="\$2"; shift 2 ;;
|
|
373
|
+
--repo=*) _ao_repo="\${1#--repo=}"; shift ;;
|
|
374
|
+
--search|--state|--assignee|--label|--jq|--template)
|
|
375
|
+
_ao_cacheable=false; break ;;
|
|
376
|
+
--search=*|--state=*|--assignee=*|--label=*|--jq=*|--template=*)
|
|
377
|
+
_ao_cacheable=false; break ;;
|
|
378
|
+
-*) shift ;; # skip unknown flags
|
|
379
|
+
*) shift ;; # skip positional
|
|
380
|
+
esac
|
|
381
|
+
done
|
|
382
|
+
set -- "\${_ao_saved_args[@]}"
|
|
383
|
+
|
|
384
|
+
if [[ "\$_ao_cacheable" == true && "\$_ao_limit" == "1" && -n "\$_ao_head" ]]; then
|
|
385
|
+
# Use sha256 hash suffix to avoid collisions from tr-based sanitization
|
|
386
|
+
# (e.g. feat/foo, feat-foo, feat_foo would otherwise map to the same key)
|
|
387
|
+
_ao_raw_key="pr-discovery-\${_ao_repo}-\${_ao_head}"
|
|
388
|
+
if [[ -n "\$_ao_json" ]]; then
|
|
389
|
+
_ao_raw_key="\${_ao_raw_key}-j-\${_ao_json}"
|
|
390
|
+
fi
|
|
391
|
+
_ao_cache_key=\$(printf '%s' "\$_ao_raw_key" | shasum -a 256 | cut -c1-16)
|
|
392
|
+
_ao_cache_key="pr-disc-\${_ao_cache_key}"
|
|
393
|
+
|
|
394
|
+
if ao_cache_fresh "\$_ao_cache_key" 120 2>/dev/null; then
|
|
395
|
+
log_ao_cache "hit" "\$_ao_cache_key" 0 0 true
|
|
396
|
+
ao_cache_read "\$_ao_cache_key"
|
|
397
|
+
exit 0
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
# Cache miss — call real gh, cache positive results (stderr passes through)
|
|
401
|
+
_ao_tmpout="\$(mktemp)"
|
|
402
|
+
trap 'rm -f "\$_ao_tmpout"' EXIT
|
|
403
|
+
_ao_start_s=\$(date +%s)
|
|
404
|
+
"\$real_gh" "\$@" > "\$_ao_tmpout"
|
|
405
|
+
_ao_exit=\$?
|
|
406
|
+
_ao_duration_ms=\$(( (\$(date +%s) - _ao_start_s) * 1000 ))
|
|
407
|
+
_ao_ok=true; [[ \$_ao_exit -ne 0 ]] && _ao_ok=false
|
|
408
|
+
cat "\$_ao_tmpout"
|
|
409
|
+
if [[ \$_ao_exit -eq 0 ]]; then
|
|
410
|
+
_ao_trimmed=\$(tr -d '[:space:]' < "\$_ao_tmpout")
|
|
411
|
+
# Only cache non-empty positive results
|
|
412
|
+
if [[ -n "\$_ao_trimmed" && "\$_ao_trimmed" != "[]" ]]; then
|
|
413
|
+
if ao_cache_write "\$_ao_cache_key" < "\$_ao_tmpout" 2>/dev/null; then
|
|
414
|
+
log_ao_cache "miss-stored" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
415
|
+
else
|
|
416
|
+
log_ao_cache "miss-write-failed" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
417
|
+
fi
|
|
418
|
+
else
|
|
419
|
+
log_ao_cache "miss-negative" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
420
|
+
fi
|
|
421
|
+
else
|
|
422
|
+
log_ao_cache "miss-error" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
423
|
+
fi
|
|
424
|
+
exit \$_ao_exit
|
|
425
|
+
fi
|
|
426
|
+
fi
|
|
427
|
+
|
|
428
|
+
# ── 2. Issue context: gh issue view <N> ─────────────────────────────────────
|
|
429
|
+
# 300-second TTL. Caches any successful response.
|
|
430
|
+
if [[ "\$1" == "issue" && "\$2" == "view" ]]; then
|
|
431
|
+
_ao_issue_id="" _ao_json="" _ao_repo="" _ao_cacheable=true
|
|
432
|
+
_ao_saved_args=("\$@")
|
|
433
|
+
shift 2
|
|
434
|
+
# First non-flag arg is the issue identifier
|
|
435
|
+
while [[ \$# -gt 0 ]]; do
|
|
436
|
+
case "\$1" in
|
|
437
|
+
--web|--comments|--jq|--template)
|
|
438
|
+
_ao_cacheable=false; break ;;
|
|
439
|
+
--jq=*|--template=*)
|
|
440
|
+
_ao_cacheable=false; break ;;
|
|
441
|
+
--json) _ao_json="\$2"; shift 2 ;;
|
|
442
|
+
--json=*) _ao_json="\${1#--json=}"; shift ;;
|
|
443
|
+
--repo) _ao_repo="\$2"; shift 2 ;;
|
|
444
|
+
--repo=*) _ao_repo="\${1#--repo=}"; shift ;;
|
|
445
|
+
-*) shift ;;
|
|
446
|
+
*)
|
|
447
|
+
if [[ -z "\$_ao_issue_id" && "\$1" =~ ^[0-9]+$ ]]; then
|
|
448
|
+
_ao_issue_id="\$1"
|
|
449
|
+
fi
|
|
450
|
+
shift ;;
|
|
451
|
+
esac
|
|
452
|
+
done
|
|
453
|
+
set -- "\${_ao_saved_args[@]}"
|
|
454
|
+
|
|
455
|
+
if [[ "\$_ao_cacheable" == true && -n "\$_ao_issue_id" ]]; then
|
|
456
|
+
_ao_raw_key="issue-ctx-\${_ao_repo}-\${_ao_issue_id}"
|
|
457
|
+
if [[ -n "\$_ao_json" ]]; then
|
|
458
|
+
_ao_raw_key="\${_ao_raw_key}-j-\${_ao_json}"
|
|
459
|
+
fi
|
|
460
|
+
_ao_cache_key=\$(printf '%s' "\$_ao_raw_key" | shasum -a 256 | cut -c1-16)
|
|
461
|
+
_ao_cache_key="issue-\${_ao_cache_key}"
|
|
462
|
+
|
|
463
|
+
if ao_cache_fresh "\$_ao_cache_key" 300 2>/dev/null; then
|
|
464
|
+
log_ao_cache "hit" "\$_ao_cache_key" 0 0 true
|
|
465
|
+
ao_cache_read "\$_ao_cache_key"
|
|
466
|
+
exit 0
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
_ao_tmpout="\$(mktemp)"
|
|
470
|
+
trap 'rm -f "\$_ao_tmpout"' EXIT
|
|
471
|
+
_ao_start_s=\$(date +%s)
|
|
472
|
+
"\$real_gh" "\$@" > "\$_ao_tmpout"
|
|
473
|
+
_ao_exit=\$?
|
|
474
|
+
_ao_duration_ms=\$(( (\$(date +%s) - _ao_start_s) * 1000 ))
|
|
475
|
+
_ao_ok=true; [[ \$_ao_exit -ne 0 ]] && _ao_ok=false
|
|
476
|
+
cat "\$_ao_tmpout"
|
|
477
|
+
if [[ \$_ao_exit -eq 0 ]]; then
|
|
478
|
+
if ao_cache_write "\$_ao_cache_key" < "\$_ao_tmpout" 2>/dev/null; then
|
|
479
|
+
log_ao_cache "miss-stored" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
480
|
+
else
|
|
481
|
+
log_ao_cache "miss-write-failed" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
482
|
+
fi
|
|
483
|
+
else
|
|
484
|
+
log_ao_cache "miss-error" "\$_ao_cache_key" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
485
|
+
fi
|
|
486
|
+
exit \$_ao_exit
|
|
487
|
+
fi
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
# =============================================================================
|
|
491
|
+
# Write intercepts
|
|
492
|
+
# =============================================================================
|
|
493
|
+
|
|
494
|
+
case "\$1/\$2" in
|
|
495
|
+
pr/create)
|
|
496
|
+
tmpout="\$(mktemp)"
|
|
497
|
+
trap 'rm -f "\$tmpout"' EXIT
|
|
498
|
+
|
|
499
|
+
_ao_start_s=\$(date +%s)
|
|
500
|
+
"\$real_gh" "\$@" 2>&1 | tee "\$tmpout"
|
|
501
|
+
exit_code=\${PIPESTATUS[0]}
|
|
502
|
+
_ao_duration_ms=\$(( (\$(date +%s) - _ao_start_s) * 1000 ))
|
|
503
|
+
_ao_ok=true; [[ \$exit_code -ne 0 ]] && _ao_ok=false
|
|
504
|
+
|
|
505
|
+
if [[ \$exit_code -eq 0 ]]; then
|
|
506
|
+
output="\$(cat "\$tmpout")"
|
|
507
|
+
pr_url="\$(echo "\$output" | grep -Eo 'https?://[^/]+/[^/]+/[^/]+/pull/[0-9]+' | head -1)"
|
|
508
|
+
report_state="pr_created"
|
|
509
|
+
report_draft="false"
|
|
510
|
+
for arg in "\$@"; do
|
|
511
|
+
if [[ "\$arg" == "--draft" || "\$arg" == "-d" ]]; then
|
|
512
|
+
report_state="draft_pr_created"
|
|
513
|
+
report_draft="true"
|
|
514
|
+
break
|
|
515
|
+
fi
|
|
516
|
+
done
|
|
517
|
+
if [[ -n "\$pr_url" ]]; then
|
|
518
|
+
update_ao_metadata pr "\$pr_url"
|
|
519
|
+
update_ao_metadata agentReportedPrUrl "\$pr_url"
|
|
520
|
+
# Append to prs field (comma-separated list of all PR URLs for this session).
|
|
521
|
+
# Supports multiple PRs per session — same repo or different repos.
|
|
522
|
+
_ao_meta_f="\${AO_DATA_DIR}/\${AO_SESSION}.json"
|
|
523
|
+
[[ -f "\$_ao_meta_f" ]] || _ao_meta_f="\${AO_DATA_DIR}/\${AO_SESSION}"
|
|
524
|
+
if head -c1 "\$_ao_meta_f" 2>/dev/null | grep -q '{'; then
|
|
525
|
+
existing_prs="\$(jq -r '.prs // empty' "\$_ao_meta_f" 2>/dev/null || echo "")"
|
|
526
|
+
else
|
|
527
|
+
existing_prs="\$(grep '^prs=' "\$_ao_meta_f" 2>/dev/null | cut -d'=' -f2- || echo "")"
|
|
528
|
+
fi
|
|
529
|
+
if [[ -z "\$existing_prs" ]]; then
|
|
530
|
+
new_prs="\$pr_url"
|
|
531
|
+
else
|
|
532
|
+
if ! echo ",\$existing_prs," | grep -qF ",\$pr_url,"; then
|
|
533
|
+
new_prs="\$existing_prs,\$pr_url"
|
|
534
|
+
else
|
|
535
|
+
new_prs="\$existing_prs"
|
|
536
|
+
fi
|
|
537
|
+
fi
|
|
538
|
+
update_ao_metadata prs "\$new_prs"
|
|
539
|
+
fi
|
|
540
|
+
pr_number="\$(printf '%s' "\$pr_url" | grep -Eo '[0-9]+$' | head -1)"
|
|
541
|
+
if [[ -n "\$pr_number" ]]; then
|
|
542
|
+
update_ao_metadata agentReportedPrNumber "\$pr_number"
|
|
543
|
+
fi
|
|
544
|
+
update_ao_metadata agentReportedState "\$report_state"
|
|
545
|
+
update_ao_metadata agentReportedAt "\$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
546
|
+
update_ao_metadata agentReportedPrIsDraft "\$report_draft"
|
|
547
|
+
fi
|
|
548
|
+
|
|
549
|
+
log_ao_cache "passthrough" "" "\$_ao_duration_ms" "\$exit_code" "\$_ao_ok"
|
|
550
|
+
exit \$exit_code
|
|
551
|
+
;;
|
|
552
|
+
*)
|
|
553
|
+
_ao_start_s=\$(date +%s)
|
|
554
|
+
"\$real_gh" "\$@"
|
|
555
|
+
_ao_exit=\$?
|
|
556
|
+
_ao_duration_ms=\$(( (\$(date +%s) - _ao_start_s) * 1000 ))
|
|
557
|
+
_ao_ok=true; [[ \$_ao_exit -ne 0 ]] && _ao_ok=false
|
|
558
|
+
log_ao_cache "passthrough" "" "\$_ao_duration_ms" "\$_ao_exit" "\$_ao_ok"
|
|
559
|
+
exit \$_ao_exit
|
|
560
|
+
;;
|
|
561
|
+
esac
|
|
562
|
+
`;
|
|
563
|
+
/**
|
|
564
|
+
* git wrapper — intercepts branch operations to auto-update metadata.
|
|
565
|
+
* All other commands pass through transparently.
|
|
566
|
+
*
|
|
567
|
+
* Detects:
|
|
568
|
+
* - git checkout -b <branch> / git switch -c <branch> (new branch)
|
|
569
|
+
* - git checkout <branch> / git switch <branch> (existing feature branch)
|
|
570
|
+
*
|
|
571
|
+
* For existing branch switches, only updates if the branch name looks like a
|
|
572
|
+
* feature branch (contains / or -) to avoid noise from checkout of commits/tags.
|
|
573
|
+
* Matches the same heuristic as Claude Code's PostToolUse hook.
|
|
574
|
+
*/
|
|
575
|
+
const GIT_WRAPPER = `#!/usr/bin/env bash
|
|
576
|
+
# ao git wrapper — auto-updates session metadata on branch operations
|
|
577
|
+
|
|
578
|
+
# Find real git by removing our wrapper directory from PATH
|
|
579
|
+
ao_bin_dir="\$(cd "\$(dirname "\$0")" && pwd)"
|
|
580
|
+
clean_path="\$(echo "\$PATH" | tr ':' '\\n' | grep -Fxv "\$ao_bin_dir" | grep . | tr '\\n' ':')"
|
|
581
|
+
clean_path="\${clean_path%:}"
|
|
582
|
+
real_git="\$(PATH="\$clean_path" command -v git 2>/dev/null)"
|
|
583
|
+
|
|
584
|
+
if [[ -z "\$real_git" ]]; then
|
|
585
|
+
echo "ao-wrapper: git not found in PATH" >&2
|
|
586
|
+
exit 127
|
|
587
|
+
fi
|
|
588
|
+
|
|
589
|
+
# Source the metadata helper
|
|
590
|
+
source "\$ao_bin_dir/ao-metadata-helper.sh" 2>/dev/null || true
|
|
591
|
+
|
|
592
|
+
# Run real git
|
|
593
|
+
"\$real_git" "\$@"
|
|
594
|
+
exit_code=\$?
|
|
595
|
+
|
|
596
|
+
# Only update metadata on success
|
|
597
|
+
if [[ \$exit_code -eq 0 ]]; then
|
|
598
|
+
case "\$1/\$2" in
|
|
599
|
+
checkout/-b)
|
|
600
|
+
update_ao_metadata branch "\$3"
|
|
601
|
+
;;
|
|
602
|
+
switch/-c)
|
|
603
|
+
update_ao_metadata branch "\$3"
|
|
604
|
+
;;
|
|
605
|
+
checkout/*|switch/*)
|
|
606
|
+
# Existing branch switch — only track feature-looking branches (contain / or -)
|
|
607
|
+
# Skip flags (e.g. -B), HEAD, tags, commit hashes, and simple names like "main"
|
|
608
|
+
branch="\$2"
|
|
609
|
+
# If $2 is a flag, the actual branch name is in $3
|
|
610
|
+
if [[ "\$branch" == -* ]]; then branch="\$3"; fi
|
|
611
|
+
if [[ -n "\$branch" && "\$branch" != "HEAD" && "\$branch" != -* && "\$branch" == *[/-]* ]]; then
|
|
612
|
+
update_ao_metadata branch "\$branch"
|
|
613
|
+
fi
|
|
614
|
+
;;
|
|
615
|
+
esac
|
|
616
|
+
fi
|
|
617
|
+
|
|
618
|
+
exit \$exit_code
|
|
619
|
+
`;
|
|
620
|
+
// =============================================================================
|
|
621
|
+
// Node.js Wrapper Scripts (Windows)
|
|
622
|
+
// =============================================================================
|
|
623
|
+
/**
|
|
624
|
+
* Build a Node.js wrapper script for a given binary (gh or git).
|
|
625
|
+
*
|
|
626
|
+
* On Windows, bash scripts cannot be executed directly, so we generate:
|
|
627
|
+
* - <name>.cjs — the actual interception logic (Node.js, forced CJS mode)
|
|
628
|
+
* - <name>.cmd — a tiny CMD shim: @node "%~dp0<name>.cjs" %*
|
|
629
|
+
*
|
|
630
|
+
* The .js script replicates what the bash wrapper does:
|
|
631
|
+
* - gh: intercepts `gh pr create` and `gh pr merge`
|
|
632
|
+
* - git: intercepts `git checkout -b` and `git switch -c`
|
|
633
|
+
*
|
|
634
|
+
* @param name - "gh" or "git"
|
|
635
|
+
* @param realBinaryPath - Absolute path to the real binary, or empty string to
|
|
636
|
+
* resolve at runtime via PATH (excluding the wrapper dir).
|
|
637
|
+
*/
|
|
638
|
+
function buildNodeWrapper(name, realBinaryPath) {
|
|
639
|
+
if (name === "gh") {
|
|
640
|
+
return buildGhNodeWrapper();
|
|
641
|
+
}
|
|
642
|
+
return buildGitNodeWrapper();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Shared Node.js snippet: updateAoMetadata function used by both gh and git wrappers.
|
|
646
|
+
* Validates session, key, and AO_DATA_DIR before writing metadata.
|
|
647
|
+
*/
|
|
648
|
+
const NODE_UPDATE_AO_METADATA = `\
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Metadata update (shared by gh/git wrappers)
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
function updateAoMetadata(key, value) {
|
|
653
|
+
const aoDir = process.env["AO_DATA_DIR"] || "";
|
|
654
|
+
const aoSession = process.env["AO_SESSION"] || "";
|
|
655
|
+
if (!aoDir || !aoSession) return;
|
|
656
|
+
|
|
657
|
+
// Validate session — no path separators or traversal
|
|
658
|
+
if (aoSession.includes("/") || aoSession.includes("\\\\") || aoSession.includes("..")) return;
|
|
659
|
+
|
|
660
|
+
// Validate key
|
|
661
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(key)) return;
|
|
662
|
+
|
|
663
|
+
// Validate aoDir is under expected locations (mirrors bash ao-metadata-helper.sh)
|
|
664
|
+
const os = require("os");
|
|
665
|
+
const home = os.homedir();
|
|
666
|
+
const sep = path.sep;
|
|
667
|
+
let resolvedDir;
|
|
668
|
+
try { resolvedDir = fs.realpathSync(aoDir); } catch { resolvedDir = path.resolve(aoDir); }
|
|
669
|
+
const allowed = [path.join(home, ".ao"), path.join(home, ".agent-orchestrator"), os.tmpdir()];
|
|
670
|
+
if (!allowed.some(a => resolvedDir === a || resolvedDir.startsWith(a + sep))) return;
|
|
671
|
+
|
|
672
|
+
// Try V2 (.json) first, then fall back to V1 (bare) — mirrors bash ao-metadata-helper.sh
|
|
673
|
+
let metadataFile = path.join(resolvedDir, aoSession + ".json");
|
|
674
|
+
if (!fs.existsSync(metadataFile)) {
|
|
675
|
+
metadataFile = path.join(resolvedDir, aoSession);
|
|
676
|
+
}
|
|
677
|
+
if (!fs.existsSync(metadataFile)) return;
|
|
678
|
+
|
|
679
|
+
// Strip newlines from value
|
|
680
|
+
const cleanValue = String(value).replace(/[\\r\\n]/g, "");
|
|
681
|
+
|
|
682
|
+
let content;
|
|
683
|
+
try { content = fs.readFileSync(metadataFile, "utf8"); } catch { return; }
|
|
684
|
+
|
|
685
|
+
const tmpFile = metadataFile + ".tmp." + process.pid;
|
|
686
|
+
try {
|
|
687
|
+
if (metadataFile.endsWith(".json")) {
|
|
688
|
+
// V2 JSON format
|
|
689
|
+
let d;
|
|
690
|
+
try { d = JSON.parse(content); } catch { return; }
|
|
691
|
+
d[key] = cleanValue;
|
|
692
|
+
fs.writeFileSync(tmpFile, JSON.stringify(d, null, 2), "utf8");
|
|
693
|
+
} else {
|
|
694
|
+
// V1 key=value format
|
|
695
|
+
const lines = content.split("\\n");
|
|
696
|
+
const keyPrefix = key + "=";
|
|
697
|
+
const idx = lines.findIndex(l => l.startsWith(keyPrefix));
|
|
698
|
+
if (idx >= 0) {
|
|
699
|
+
lines[idx] = key + "=" + cleanValue;
|
|
700
|
+
} else {
|
|
701
|
+
lines.push(key + "=" + cleanValue);
|
|
702
|
+
}
|
|
703
|
+
fs.writeFileSync(tmpFile, lines.join("\\n"), "utf8");
|
|
704
|
+
}
|
|
705
|
+
fs.renameSync(tmpFile, metadataFile);
|
|
706
|
+
} catch {
|
|
707
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
708
|
+
}
|
|
709
|
+
}`;
|
|
710
|
+
function buildGhNodeWrapper(realBinaryPath) {
|
|
711
|
+
return `#!/usr/bin/env node
|
|
712
|
+
// ao gh wrapper (Windows Node.js) — auto-updates session metadata on PR operations
|
|
713
|
+
"use strict";
|
|
714
|
+
const { spawnSync } = require("child_process");
|
|
715
|
+
const fs = require("fs");
|
|
716
|
+
const path = require("path");
|
|
717
|
+
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
// Real binary resolution
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
const AO_BIN_DIR = path.dirname(__filename);
|
|
722
|
+
|
|
723
|
+
function findRealGh() {
|
|
724
|
+
const explicit = process.env["GH_PATH"] || "";
|
|
725
|
+
if (explicit) {
|
|
726
|
+
try {
|
|
727
|
+
const resolved = path.resolve(explicit);
|
|
728
|
+
const dir = path.dirname(resolved);
|
|
729
|
+
if (dir !== AO_BIN_DIR && fs.existsSync(resolved)) return resolved;
|
|
730
|
+
} catch {}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Walk PATH, skip wrapper directory
|
|
734
|
+
const pathDirs = (process.env["PATH"] || "").split(path.delimiter);
|
|
735
|
+
for (const dir of pathDirs) {
|
|
736
|
+
if (!dir || path.resolve(dir) === AO_BIN_DIR) continue;
|
|
737
|
+
// Windows executables always have an extension (.exe/.cmd). Skip the bare
|
|
738
|
+
// no-extension case — on Windows X_OK is identical to F_OK (execute bit
|
|
739
|
+
// doesn't exist), so a bare text file named "gh" would otherwise be
|
|
740
|
+
// selected before gh.exe.
|
|
741
|
+
for (const ext of [".exe", ".cmd"]) {
|
|
742
|
+
const candidate = path.join(dir, "gh" + ext);
|
|
743
|
+
try {
|
|
744
|
+
fs.accessSync(candidate, fs.constants.F_OK);
|
|
745
|
+
return candidate;
|
|
746
|
+
} catch {}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
${NODE_UPDATE_AO_METADATA}
|
|
753
|
+
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// Main
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
const realGh = ${"findRealGh()"};
|
|
758
|
+
if (!realGh) {
|
|
759
|
+
process.stderr.write("ao-wrapper: gh not found in PATH\\n");
|
|
760
|
+
process.exit(127);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const args = process.argv.slice(2);
|
|
764
|
+
const sub1 = args[0] || "";
|
|
765
|
+
const sub2 = args[1] || "";
|
|
766
|
+
const key = sub1 + "/" + sub2;
|
|
767
|
+
|
|
768
|
+
if (key === "pr/create" || key === "pr/merge") {
|
|
769
|
+
const result = spawnSync(realGh, args, {
|
|
770
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
771
|
+
encoding: "utf8",
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
775
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
776
|
+
|
|
777
|
+
if (result.status === 0) {
|
|
778
|
+
const output = (result.stdout || "") + (result.stderr || "");
|
|
779
|
+
if (key === "pr/create") {
|
|
780
|
+
const match = output.match(/https:\\/\\/github\\.com\\/[^/]+\\/[^/]+\\/pull\\/[0-9]+/);
|
|
781
|
+
if (match) {
|
|
782
|
+
const prUrl = match[0];
|
|
783
|
+
updateAoMetadata("pr", prUrl);
|
|
784
|
+
updateAoMetadata("status", "pr_open");
|
|
785
|
+
// Append to prs field — supports multiple PRs per session
|
|
786
|
+
let existingPrs = "";
|
|
787
|
+
try {
|
|
788
|
+
const aoDir = process.env["AO_DATA_DIR"] || "";
|
|
789
|
+
const aoSession = process.env["AO_SESSION"] || "";
|
|
790
|
+
if (aoDir && aoSession && /^[a-zA-Z0-9_-]+$/.test(aoSession)) {
|
|
791
|
+
let metaFile = path.join(aoDir, aoSession + ".json");
|
|
792
|
+
if (!fs.existsSync(metaFile)) metaFile = path.join(aoDir, aoSession);
|
|
793
|
+
if (fs.existsSync(metaFile)) {
|
|
794
|
+
const raw = fs.readFileSync(metaFile, "utf8");
|
|
795
|
+
if (metaFile.endsWith(".json")) {
|
|
796
|
+
existingPrs = JSON.parse(raw).prs || "";
|
|
797
|
+
} else {
|
|
798
|
+
const line = raw.split("\\n").find(l => l.startsWith("prs="));
|
|
799
|
+
existingPrs = line ? line.slice(4) : "";
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch {}
|
|
804
|
+
const newPrs = existingPrs
|
|
805
|
+
? existingPrs.split(",").map((u) => u.trim()).includes(prUrl)
|
|
806
|
+
? existingPrs
|
|
807
|
+
: existingPrs + "," + prUrl
|
|
808
|
+
: prUrl;
|
|
809
|
+
updateAoMetadata("prs", newPrs);
|
|
810
|
+
}
|
|
811
|
+
} else if (key === "pr/merge") {
|
|
812
|
+
updateAoMetadata("status", "merged");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
process.exit(result.status ?? 1);
|
|
817
|
+
} else {
|
|
818
|
+
const result = spawnSync(realGh, args, { stdio: "inherit" });
|
|
819
|
+
process.exit(result.status ?? 1);
|
|
820
|
+
}
|
|
821
|
+
`;
|
|
822
|
+
}
|
|
823
|
+
function buildGitNodeWrapper(realBinaryPath) {
|
|
824
|
+
return `#!/usr/bin/env node
|
|
825
|
+
// ao git wrapper (Windows Node.js) — auto-updates session metadata on branch operations
|
|
826
|
+
"use strict";
|
|
827
|
+
const { spawnSync } = require("child_process");
|
|
828
|
+
const fs = require("fs");
|
|
829
|
+
const path = require("path");
|
|
830
|
+
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// Real binary resolution
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
const AO_BIN_DIR = path.dirname(__filename);
|
|
835
|
+
|
|
836
|
+
function findRealGit() {
|
|
837
|
+
const pathDirs = (process.env["PATH"] || "").split(path.delimiter);
|
|
838
|
+
for (const dir of pathDirs) {
|
|
839
|
+
if (!dir || path.resolve(dir) === AO_BIN_DIR) continue;
|
|
840
|
+
// Windows executables always have an extension (.exe/.cmd). Skip the bare
|
|
841
|
+
// no-extension case — on Windows X_OK is identical to F_OK (execute bit
|
|
842
|
+
// doesn't exist), so a bare text file named "git" would otherwise be
|
|
843
|
+
// selected before git.exe.
|
|
844
|
+
for (const ext of [".exe", ".cmd"]) {
|
|
845
|
+
const candidate = path.join(dir, "git" + ext);
|
|
846
|
+
try {
|
|
847
|
+
fs.accessSync(candidate, fs.constants.F_OK);
|
|
848
|
+
return candidate;
|
|
849
|
+
} catch {}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
${NODE_UPDATE_AO_METADATA}
|
|
856
|
+
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
// Main
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
const realGit = ${"findRealGit()"};
|
|
861
|
+
if (!realGit) {
|
|
862
|
+
process.stderr.write("ao-wrapper: git not found in PATH\\n");
|
|
863
|
+
process.exit(127);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const args = process.argv.slice(2);
|
|
867
|
+
const result = spawnSync(realGit, args, { stdio: "inherit" });
|
|
868
|
+
const exitCode = result.status ?? 1;
|
|
869
|
+
|
|
870
|
+
if (exitCode === 0) {
|
|
871
|
+
const sub1 = args[0] || "";
|
|
872
|
+
const sub2 = args[1] || "";
|
|
873
|
+
const key = sub1 + "/" + sub2;
|
|
874
|
+
|
|
875
|
+
if (key === "checkout/-b" || key === "switch/-c") {
|
|
876
|
+
const branch = args[2];
|
|
877
|
+
if (branch) updateAoMetadata("branch", branch);
|
|
878
|
+
} else if (sub1 === "checkout" || sub1 === "switch") {
|
|
879
|
+
// Existing branch switch — only track feature-looking branches (contain / or -)
|
|
880
|
+
let branch = sub2;
|
|
881
|
+
// If sub2 is a flag, the actual branch name is in args[2]
|
|
882
|
+
if (branch && branch.startsWith("-")) branch = args[2] || "";
|
|
883
|
+
if (
|
|
884
|
+
branch &&
|
|
885
|
+
branch !== "HEAD" &&
|
|
886
|
+
!branch.startsWith("-") &&
|
|
887
|
+
(branch.includes("/") || branch.includes("-"))
|
|
888
|
+
) {
|
|
889
|
+
updateAoMetadata("branch", branch);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
process.exit(exitCode);
|
|
895
|
+
`;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Section appended to AGENTS.md as a secondary signal. The PATH-based wrappers
|
|
899
|
+
* handle metadata updates automatically, but AGENTS.md reinforces the intent
|
|
900
|
+
* and helps if the wrappers are bypassed.
|
|
901
|
+
*/
|
|
902
|
+
const AO_AGENTS_MD_SECTION = `
|
|
903
|
+
## Athene (ao) Session
|
|
904
|
+
|
|
905
|
+
You are running inside an Athene managed workspace.
|
|
906
|
+
Session metadata is updated automatically via shell wrappers.
|
|
907
|
+
|
|
908
|
+
If automatic updates fail, you can manually update metadata:
|
|
909
|
+
\`\`\`bash
|
|
910
|
+
~/.ao/bin/ao-metadata-helper.sh # sourced automatically
|
|
911
|
+
# Then call: update_ao_metadata <key> <value>
|
|
912
|
+
\`\`\`
|
|
913
|
+
`;
|
|
914
|
+
/* eslint-enable no-useless-escape */
|
|
915
|
+
// =============================================================================
|
|
916
|
+
// Workspace Setup
|
|
917
|
+
// =============================================================================
|
|
918
|
+
/**
|
|
919
|
+
* Atomically write a file by writing to a temp file in the same directory,
|
|
920
|
+
* then renaming. Prevents concurrent sessions from reading partially written scripts.
|
|
921
|
+
*/
|
|
922
|
+
async function atomicWriteFile(filePath, content, mode) {
|
|
923
|
+
const suffix = randomBytes(6).toString("hex");
|
|
924
|
+
const tmpPath = `${filePath}.tmp.${suffix}`;
|
|
925
|
+
await writeFile(tmpPath, content, { encoding: "utf-8", mode });
|
|
926
|
+
await rename(tmpPath, filePath);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Install PATH-based shell wrappers and append an AO section to AGENTS.md.
|
|
930
|
+
*
|
|
931
|
+
* This is the standard workspace setup for agents that don't have native hook
|
|
932
|
+
* systems (Codex, Aider, OpenCode). Call this from both `setupWorkspaceHooks`
|
|
933
|
+
* and `postLaunchSetup`.
|
|
934
|
+
*
|
|
935
|
+
* 1. Creates ~/.ao/bin/ with gh/git wrappers and metadata helper script
|
|
936
|
+
* 2. Appends an "Athene" section to the workspace AGENTS.md
|
|
937
|
+
*/
|
|
938
|
+
async function setupPathWrapperWorkspace(workspacePath) {
|
|
939
|
+
// 1. Write shared wrappers to ~/.ao/bin/ (skip if version marker matches)
|
|
940
|
+
await mkdir(getAoBinDir(), { recursive: true });
|
|
941
|
+
const markerPath = join(getAoBinDir(), ".ao-version");
|
|
942
|
+
let needsUpdate = true;
|
|
943
|
+
try {
|
|
944
|
+
const existing = await readFile(markerPath, "utf-8");
|
|
945
|
+
if (existing.trim() === WRAPPER_VERSION)
|
|
946
|
+
needsUpdate = false;
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
// File doesn't exist — needs update
|
|
950
|
+
}
|
|
951
|
+
if (needsUpdate) {
|
|
952
|
+
if (isWindows()) {
|
|
953
|
+
// On Windows: generate Node.js .js wrappers + .cmd shims.
|
|
954
|
+
// Bash scripts can't be executed directly on Windows.
|
|
955
|
+
// Write wrappers atomically, then write the version marker last.
|
|
956
|
+
for (const name of ["gh", "git"]) {
|
|
957
|
+
const wrapperBase = join(getAoBinDir(), name);
|
|
958
|
+
const nodeScript = buildNodeWrapper(name);
|
|
959
|
+
// Use .cjs extension to force CJS mode regardless of any parent package.json "type" field
|
|
960
|
+
await atomicWriteFile(wrapperBase + ".cjs", nodeScript, 0o644);
|
|
961
|
+
// .cmd shim: delegates to node <wrapper>.cjs forwarding all args
|
|
962
|
+
await atomicWriteFile(wrapperBase + ".cmd", `@node "%~dp0${name}.cjs" %*\r\n`, 0o644);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
await atomicWriteFile(join(getAoBinDir(), "ao-metadata-helper.sh"), AO_METADATA_HELPER, 0o755);
|
|
967
|
+
// Write wrappers atomically, then write the version marker last.
|
|
968
|
+
// If we crash between wrapper writes and marker write, the next
|
|
969
|
+
// invocation will redo the writes (safe: wrappers are idempotent).
|
|
970
|
+
await atomicWriteFile(join(getAoBinDir(), "gh"), GH_WRAPPER, 0o755);
|
|
971
|
+
await atomicWriteFile(join(getAoBinDir(), "git"), GIT_WRAPPER, 0o755);
|
|
972
|
+
}
|
|
973
|
+
await atomicWriteFile(markerPath, WRAPPER_VERSION, 0o644);
|
|
974
|
+
}
|
|
975
|
+
// 2. Write AO session context to .ao/AGENTS.md (gitignored) so agents
|
|
976
|
+
// can discover they're in a managed session. We don't modify the
|
|
977
|
+
// repo-tracked AGENTS.md to avoid polluting worktrees with dirty state.
|
|
978
|
+
const aoAgentsMdPath = join(workspacePath, ".ao", "AGENTS.md");
|
|
979
|
+
await mkdir(join(workspacePath, ".ao"), { recursive: true });
|
|
980
|
+
// On Windows, ao-metadata-helper.sh is never created — use a platform-appropriate section
|
|
981
|
+
const agentsMdContent = isWindows()
|
|
982
|
+
? `## Athene (ao) Session\n\nYou are running inside an Athene managed workspace.\nSession metadata is updated automatically via shell wrappers.\n`
|
|
983
|
+
: AO_AGENTS_MD_SECTION.trimStart();
|
|
984
|
+
await writeFile(aoAgentsMdPath, agentsMdContent, "utf-8");
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export { AO_AGENTS_MD_SECTION, AO_METADATA_HELPER, GH_WRAPPER, GIT_WRAPPER, PREFERRED_GH_PATH, buildAgentPath, buildNodeWrapper, setupPathWrapperWorkspace };
|
|
988
|
+
//# sourceMappingURL=agent-workspace-hooks.js.map
|