@amsterdamdatalabs/enact-extensions 0.1.1 → 0.1.5

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.
Files changed (115) hide show
  1. package/README.md +4 -3
  2. package/dist/index.d.ts +5 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/install.d.ts +82 -1
  7. package/dist/install.d.ts.map +1 -1
  8. package/dist/install.js +187 -35
  9. package/dist/install.js.map +1 -1
  10. package/dist/internal/codex.d.ts.map +1 -1
  11. package/dist/internal/codex.js +7 -1
  12. package/dist/internal/codex.js.map +1 -1
  13. package/dist/internal/platform.d.ts +8 -0
  14. package/dist/internal/platform.d.ts.map +1 -1
  15. package/dist/internal/platform.js +46 -2
  16. package/dist/internal/platform.js.map +1 -1
  17. package/dist/provision.d.ts +30 -0
  18. package/dist/provision.d.ts.map +1 -0
  19. package/dist/provision.js +202 -0
  20. package/dist/provision.js.map +1 -0
  21. package/dist/validate/index.d.ts +23 -0
  22. package/dist/validate/index.d.ts.map +1 -1
  23. package/dist/validate/index.js +80 -0
  24. package/dist/validate/index.js.map +1 -1
  25. package/extensions/enact-context/.agents/plugin.json +40 -0
  26. package/extensions/enact-context/.mcp.json +8 -0
  27. package/extensions/enact-context/README.md +25 -0
  28. package/extensions/enact-context/assets/icon.png +0 -0
  29. package/extensions/enact-context/assets/logo.png +0 -0
  30. package/extensions/enact-context/hooks/hooks.json +105 -0
  31. package/extensions/enact-context/skills/enact-context/SKILL.md +149 -0
  32. package/extensions/enact-context/skills/enact-context/scripts/install.sh +69 -0
  33. package/extensions/enact-factory/.agents/plugin.json +42 -0
  34. package/extensions/enact-factory/.mcp.json +8 -0
  35. package/extensions/enact-factory/assets/icon.png +0 -0
  36. package/extensions/enact-factory/assets/logo.png +0 -0
  37. package/extensions/enact-factory/hooks/user-prompt-submit.mjs +67 -0
  38. package/extensions/enact-factory/skills/testing-strategy/SKILL.md +167 -0
  39. package/extensions/enact-factory/skills/workitem-triage/SKILL.md +22 -0
  40. package/extensions/enact-operator/.agents/plugin.json +57 -0
  41. package/extensions/enact-operator/.app.json +3 -0
  42. package/extensions/enact-operator/.mcp.json +10 -0
  43. package/extensions/enact-operator/_taxonomy.md +86 -0
  44. package/extensions/enact-operator/agents/README.md +5 -0
  45. package/extensions/enact-operator/agents/architect.toml +25 -0
  46. package/extensions/enact-operator/agents/code-reviewer.toml +24 -0
  47. package/extensions/enact-operator/agents/critic.toml +30 -0
  48. package/extensions/enact-operator/agents/executor.toml +24 -0
  49. package/extensions/enact-operator/agents/explore.toml +23 -0
  50. package/extensions/enact-operator/agents/planner.toml +24 -0
  51. package/extensions/enact-operator/agents/verifier.toml +24 -0
  52. package/extensions/enact-operator/assets/icon.png +0 -0
  53. package/extensions/enact-operator/assets/logo.png +0 -0
  54. package/extensions/enact-operator/commands/doctor.md +39 -0
  55. package/extensions/enact-operator/commands/setup.md +51 -0
  56. package/extensions/enact-operator/hooks/hooks.json +126 -0
  57. package/extensions/enact-operator/skills/_variants.md +44 -0
  58. package/extensions/enact-operator/skills/ai-slop-cleaner/SKILL.md +50 -0
  59. package/extensions/enact-operator/skills/analyze/SKILL.md +91 -0
  60. package/extensions/enact-operator/skills/ask/SKILL.md +47 -0
  61. package/extensions/enact-operator/skills/autopilot/SKILL.md +170 -0
  62. package/extensions/enact-operator/skills/autoresearch-goal/SKILL.md +79 -0
  63. package/extensions/enact-operator/skills/cancel/SKILL.md +99 -0
  64. package/extensions/enact-operator/skills/configure-notifications/SKILL.md +77 -0
  65. package/extensions/enact-operator/skills/deep-interview/SKILL.md +80 -0
  66. package/extensions/enact-operator/skills/doctor/SKILL.md +48 -0
  67. package/extensions/enact-operator/skills/hud/SKILL.md +49 -0
  68. package/extensions/enact-operator/skills/hyperplan/SKILL.md +47 -0
  69. package/extensions/enact-operator/skills/plan/SKILL.md +78 -0
  70. package/extensions/enact-operator/skills/ralph/SKILL.md +201 -0
  71. package/extensions/enact-operator/skills/ralph/gemini.md +18 -0
  72. package/extensions/enact-operator/skills/ralplan/SKILL.md +151 -0
  73. package/extensions/enact-operator/skills/remove-deadcode/SKILL.md +45 -0
  74. package/extensions/enact-operator/skills/research/SKILL.md +74 -0
  75. package/extensions/enact-operator/skills/review/SKILL.md +58 -0
  76. package/extensions/enact-operator/skills/security-research/SKILL.md +54 -0
  77. package/extensions/enact-operator/skills/setup/SKILL.md +91 -0
  78. package/extensions/enact-operator/skills/setup/scripts/install.sh +50 -0
  79. package/extensions/enact-operator/skills/skill/SKILL.md +82 -0
  80. package/extensions/enact-operator/skills/tdd/SKILL.md +59 -0
  81. package/extensions/enact-operator/skills/team/SKILL.md +199 -0
  82. package/extensions/enact-operator/skills/trace/SKILL.md +41 -0
  83. package/extensions/enact-operator/skills/ultragoal/SKILL.md +99 -0
  84. package/extensions/enact-operator/skills/ultraqa/SKILL.md +113 -0
  85. package/extensions/enact-operator/skills/ultrawork/SKILL.md +145 -0
  86. package/extensions/enact-operator/skills/ultrawork/planner.md +28 -0
  87. package/extensions/enact-operator/skills/wiki/SKILL.md +41 -0
  88. package/extensions/enact-operator/skills/work-with-workitem/SKILL.md +51 -0
  89. package/extensions/enact-wiki/.agents/plugin.json +42 -0
  90. package/extensions/enact-wiki/.mcp.json +15 -0
  91. package/extensions/enact-wiki/README.md +44 -0
  92. package/extensions/enact-wiki/assets/icon.png +0 -0
  93. package/extensions/enact-wiki/assets/logo.png +0 -0
  94. package/extensions/enact-wiki/skills/document-parser/SKILL.md +17 -0
  95. package/extensions/enact-wiki/skills/document-parser/scripts/parse.sh +60 -0
  96. package/extensions/enact-wiki/skills/document-parser/skill.json +9 -0
  97. package/extensions/enact-wiki/skills/enact-wiki/SKILL.md +30 -0
  98. package/extensions/enact-wiki/skills/enact-wiki/references/ingest.md +62 -0
  99. package/extensions/enact-wiki/skills/enact-wiki/references/manage.md +34 -0
  100. package/extensions/enact-wiki/skills/enact-wiki/references/query.md +59 -0
  101. package/extensions/enact-wiki/skills/search-lab/SKILL.md +57 -0
  102. package/extensions/enact-wiki/skills/search-lab/scripts/analyze.ts +23 -0
  103. package/package.json +1 -1
  104. package/scripts/enact-extensions.mjs +79 -12
  105. package/scripts/lib/hooks.mjs +352 -0
  106. package/scripts/lib/ledger.mjs +4 -3
  107. package/scripts/lib/provision-mcp.mjs +12 -365
  108. package/scripts/lib/run-install.mjs +87 -5
  109. package/scripts/lib/run-prune.mjs +73 -0
  110. package/scripts/lib/run-sync.mjs +9 -1
  111. package/scripts/lib/run-uninstall.mjs +26 -2
  112. package/scripts/lib/run-validate.mjs +10 -1
  113. package/scripts/lib/serve.mjs +19 -1
  114. package/scripts/version-bump.sh +463 -0
  115. package/spec/codex.json +1 -11
@@ -9,6 +9,7 @@ import {
9
9
  defaultSharedHome,
10
10
  } from "../../dist/index.js";
11
11
  import { appendEntry } from "./ledger.mjs";
12
+ import { removePluginHooks } from "./hooks.mjs";
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // resolveHomes — mirrors run-install.mjs; same scope/per-platform-home logic.
@@ -19,7 +20,7 @@ function resolveHomes(options) {
19
20
  return {
20
21
  scope,
21
22
  codexHome: options.codexHome ?? (scope === "local" ? local(".codex") : undefined),
22
- enactHome: options.enactHome ?? (scope === "local" ? local(".enact") : defaultEnactHome()),
23
+ enactHome: options.enactHome ?? (scope === "local" ? local(".enact/agent") : defaultEnactHome()),
23
24
  claudeHome: options.claudeHome ?? (scope === "local" ? local(".claude") : undefined),
24
25
  cursorHome: options.cursorHome ?? (scope === "local" ? local(".cursor") : undefined),
25
26
  sharedHome: options.sharedHome ?? (scope === "local" ? process.cwd() : defaultSharedHome()),
@@ -29,7 +30,7 @@ function resolveHomes(options) {
29
30
  // ---------------------------------------------------------------------------
30
31
  // ALL_PLATFORMS / parsePlatforms — reuse same logic as run-install.mjs
31
32
  // ---------------------------------------------------------------------------
32
- const ALL_PLATFORMS = ["codex", "claude", "cursor", "enact", "shared"];
33
+ const ALL_PLATFORMS = ["codex", "claude", "cursor", "enact"];
33
34
 
34
35
  /**
35
36
  * Expand "all" and split comma-separated platform lists.
@@ -83,11 +84,26 @@ function recordUninstall(name, version, platform, homes, options, result) {
83
84
  scope: homes.scope,
84
85
  home: homes[platformHomeField(platform)] ?? homes.enactHome,
85
86
  path,
87
+ marketplaceName: options.marketplaceName ?? "enact-os-plugins",
86
88
  },
87
89
  options,
88
90
  );
89
91
  }
90
92
 
93
+ function removeInstalledPluginHooks(name, platform, homes, options) {
94
+ if (!["codex", "claude", "cursor", "enact"].includes(platform)) return;
95
+ const hookResult = removePluginHooks(platform, name, {
96
+ cwd: options.cwd ?? process.cwd(),
97
+ codexHome: platform === "codex" ? homes.codexHome : undefined,
98
+ claudeHome: platform === "claude" ? homes.claudeHome : undefined,
99
+ cursorHome: platform === "cursor" ? homes.cursorHome : undefined,
100
+ enactHome: platform === "enact" ? homes.enactHome : undefined,
101
+ });
102
+ if (hookResult.result !== "not_found" && hookResult.result !== "skipped") {
103
+ console.log(`hooks ${hookResult.result} for ${name} on ${platform} -> ${hookResult.location}`);
104
+ }
105
+ }
106
+
91
107
  function platformHomeField(platform) {
92
108
  switch (platform) {
93
109
  case "enact": return "enactHome";
@@ -109,6 +125,11 @@ function platformHomeField(platform) {
109
125
  function runSinglePlatform(name, platform, homes, options) {
110
126
  let result;
111
127
 
128
+ // Remove plugin hook registrations before platform uninstall rewrites host
129
+ // config files. Some serializers preserve hook tables but drop marker
130
+ // comments, so post-uninstall cleanup would lose its reversible marker.
131
+ removeInstalledPluginHooks(name, platform, homes, options);
132
+
112
133
  switch (platform) {
113
134
  case "enact":
114
135
  result = uninstallCodexPluginBundle(name, {
@@ -143,6 +164,7 @@ function runSinglePlatform(name, platform, homes, options) {
143
164
  }
144
165
 
145
166
  if (result.noop) {
167
+ removeInstalledPluginHooks(name, platform, homes, options);
146
168
  process.stderr.write(
147
169
  `[enact-extensions uninstall] Nothing to remove for ${name} on ${platform} (not installed).\n`,
148
170
  );
@@ -154,6 +176,8 @@ function runSinglePlatform(name, platform, homes, options) {
154
176
  }
155
177
  console.log(`uninstalled ${name} from ${platform}`);
156
178
 
179
+ removeInstalledPluginHooks(name, platform, homes, options);
180
+
157
181
  // Record uninstall in ledger (best-effort)
158
182
  recordUninstall(name, options.version, platform, homes, options, result);
159
183
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  validatePluginBundleFromCanonical,
3
3
  checkPluginBundleComponentPathsFromCanonical,
4
+ checkHookEvents,
4
5
  } from "../../dist/index.js";
5
6
 
6
7
  /**
@@ -13,6 +14,7 @@ import {
13
14
  export function runValidate(pluginRoot, platforms) {
14
15
  const report = validatePluginBundleFromCanonical(pluginRoot, platforms);
15
16
  const warnings = checkPluginBundleComponentPathsFromCanonical(pluginRoot);
17
+ const hookErrors = checkHookEvents(pluginRoot);
16
18
 
17
19
  for (const result of report.results) {
18
20
  const status = result.ok ? "ok" : "FAIL";
@@ -22,9 +24,16 @@ export function runValidate(pluginRoot, platforms) {
22
24
  }
23
25
  }
24
26
 
27
+ if (hookErrors.length > 0) {
28
+ console.log(`[FAIL] hooks (${pluginRoot})`);
29
+ for (const err of hookErrors) {
30
+ console.log(` - ${err}`);
31
+ }
32
+ }
33
+
25
34
  for (const warn of warnings) {
26
35
  console.log(`[warn] ${warn}`);
27
36
  }
28
37
 
29
- return report.ok && warnings.length === 0;
38
+ return report.ok && warnings.length === 0 && hookErrors.length === 0;
30
39
  }
@@ -436,9 +436,27 @@ export function createServer(opts = {}) {
436
436
  * @param {object} [opts.installDefaults] - Merged into runInstall/runUninstall calls.
437
437
  * @returns {Promise<{ server: http.Server, url: string }>}
438
438
  */
439
+ // ENACT OS port-registry convention: dev listeners live in 43xxx, prod in 53xxx.
440
+ export const DEV_PORT = 43217;
441
+ export const PROD_PORT = 53217;
442
+
443
+ /**
444
+ * Resolve the serve port: an explicit `--port` always wins; otherwise `--prod`
445
+ * selects the prod-series port (53217) and the default is the dev-series port
446
+ * (43217). Pure (no I/O) so it is unit-testable without binding a socket.
447
+ * @param {object} [opts]
448
+ * @param {number} [opts.port] - Explicit port (wins; 0 = ephemeral).
449
+ * @param {boolean} [opts.prod] - Use the prod-series port (53217).
450
+ * @returns {number}
451
+ */
452
+ export function resolveServePort(opts = {}) {
453
+ if (opts.port !== undefined && opts.port !== null) return opts.port;
454
+ return opts.prod ? PROD_PORT : DEV_PORT;
455
+ }
456
+
439
457
  export function startServer(opts = {}) {
440
458
  const host = opts.host ?? "127.0.0.1";
441
- const port = opts.port ?? 43217;
459
+ const port = resolveServePort(opts);
442
460
 
443
461
  const server = createServer(opts);
444
462
 
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # version-bump.sh
4
+ # Auto-bumps patch version (x.y.z → x.y.z+1) on a feat/* or fix/* branch,
5
+ # as the LAST CI step before the PR merges into integration.
6
+ #
7
+ # Strategy (runs on the feature branch, not on integration):
8
+ # 1. Must be on a feat/* or fix/* branch — not on integration itself.
9
+ # 2. Fetches origin/integration and compares versions:
10
+ # Same → PR did not manually bump → auto-bump on the feature branch.
11
+ # Different → developer bumped intentionally in the PR → skip silently.
12
+ # 3. Commits + pushes the bump back to the SAME feature branch (***NO_CI***
13
+ # prevents a second pipeline run). The PR then merges to integration
14
+ # already carrying the bumped version.
15
+ #
16
+ # Why not push to integration directly?
17
+ # Azure DevOps branch policy (TF402455) blocks direct pushes to protected
18
+ # branches. We never bypass this — instead we bump on the feature branch.
19
+ #
20
+ # Supported manifest types — detected from filename, no scanning:
21
+ # package.json → npm (.version)
22
+ # pyproject.toml → python ([project] version, uv / PEP 621)
23
+ # Cargo.toml → rust ([package] version)
24
+ #
25
+ # Usage:
26
+ # ./scripts/version-bump.sh --file <path/to/manifest> [OPTIONS]
27
+ #
28
+ # Options:
29
+ # -f, --file <path> REQUIRED. Exact path to the version manifest.
30
+ # -b, --branch <name> Integration branch to compare against (default: integration)
31
+ # --push Push the bump commit back to the feature branch
32
+ # -n, --dry-run Print what would change; no edits, no commit
33
+ # -h, --help Show this help
34
+ # ─────────────────────────────────────────────────────────────────────────────
35
+ set -euo pipefail
36
+
37
+ # ── ANSI colours ──────────────────────────────────────────────────────────────
38
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
39
+ BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
40
+ info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; }
41
+ ok() { printf "${GREEN}[OK]${NC} %s\n" "$*"; }
42
+ warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
43
+ die() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; exit 1; }
44
+ step() { printf "\n${BOLD}▶ %s${NC}\n" "$*"; }
45
+
46
+ # ── State ─────────────────────────────────────────────────────────────────────
47
+ VERSION_FILE=""
48
+ PROJECT_TYPE=""
49
+ GIT_REPO_ROOT=""
50
+ CURRENT_VERSION=""
51
+ CURRENT_BRANCH=""
52
+ INTEGRATION_BRANCH="integration"
53
+ DRY_RUN=false
54
+ DO_PUSH=false
55
+
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+ usage() {
58
+ cat <<'HELP'
59
+ version-bump.sh — Bump patch version (x.y.z → x.y.z+1) on a feat/fix branch before merge
60
+
61
+ USAGE
62
+ ./scripts/version-bump.sh --file <path> [OPTIONS]
63
+
64
+ OPTIONS
65
+ -f, --file <path> REQUIRED. Exact path to the version manifest:
66
+ package.json → npm
67
+ pyproject.toml → python (PEP 621 / uv)
68
+ Cargo.toml → rust
69
+ -b, --branch <name> Integration branch to compare against (default: integration)
70
+ --push Push the bump commit back to the feature branch after committing
71
+ -n, --dry-run Show what would change; do not modify or commit
72
+ -h, --help Show this message
73
+
74
+ GUARDS
75
+ 1. Must be on a feat/* or fix/* branch — never runs on integration directly.
76
+ 2. Fetches origin/<integration-branch> and compares versions:
77
+ Same → PR did not manually bump the version → auto-bump.
78
+ Different → developer bumped intentionally in the PR → skip silently.
79
+
80
+ PUSH TARGET
81
+ The bump commit is pushed back to the CURRENT feature branch (not integration).
82
+ Azure DevOps branch policies block direct pushes to integration; we never bypass them.
83
+
84
+ CI SKIP
85
+ Bump commit message contains ***NO_CI*** — Azure DevOps' official keyword
86
+ to prevent the resulting push from re-triggering the pipeline.
87
+
88
+ AZURE PIPELINES — add as last step in the CI job, covering PR builds on feat/* / fix/*:
89
+
90
+ - script: |
91
+ git config user.email "pipeline@build.com"
92
+ git config user.name "Azure Pipelines"
93
+ git remote set-url origin \
94
+ "https://x-token:$(System.AccessToken)@dev.azure.com/org/project/_git/repo"
95
+ bash scripts/version-bump.sh --file package.json --push
96
+ displayName: Bump patch version
97
+ condition: |
98
+ and(
99
+ succeeded(),
100
+ or(
101
+ startsWith(variables['System.PullRequest.SourceBranch'], 'refs/heads/feat/'),
102
+ startsWith(variables['System.PullRequest.SourceBranch'], 'refs/heads/fix/'),
103
+ startsWith(variables['Build.SourceBranch'], 'refs/heads/feat/'),
104
+ startsWith(variables['Build.SourceBranch'], 'refs/heads/fix/')
105
+ ),
106
+ not(contains(variables['Build.SourceVersionMessage'], '***NO_CI***'))
107
+ )
108
+ env:
109
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
110
+ BUILD_SOURCEBRANCH: $(Build.SourceBranch)
111
+ SYSTEM_PULLREQUEST_SOURCEBRANCH: $(System.PullRequest.SourceBranch)
112
+ HELP
113
+ exit 0
114
+ }
115
+
116
+ # ─────────────────────────────────────────────────────────────────────────────
117
+ parse_args() {
118
+ local file_given=false
119
+ while [[ $# -gt 0 ]]; do
120
+ case "$1" in
121
+ -f|--file) VERSION_FILE="${2:?'--file requires a path'}"; file_given=true; shift 2 ;;
122
+ -b|--branch) INTEGRATION_BRANCH="${2:?'--branch requires a name'}"; shift 2 ;;
123
+ --push) DO_PUSH=true; shift ;;
124
+ -n|--dry-run) DRY_RUN=true; shift ;;
125
+ -h|--help) usage ;;
126
+ *) die "Unknown option: '$1'. Run with --help." ;;
127
+ esac
128
+ done
129
+ $file_given || die "--file <path> is required. Example: --file package.json"
130
+ }
131
+
132
+ # ─────────────────────────────────────────────────────────────────────────────
133
+ # Resolve manifest + detect type from filename (no scanning)
134
+ # ─────────────────────────────────────────────────────────────────────────────
135
+ resolve_manifest() {
136
+ step "Resolving manifest"
137
+ [[ -f "$VERSION_FILE" ]] || die "Manifest not found: '${VERSION_FILE}'"
138
+
139
+ # Physical absolute path (handles /tmp → /private/tmp on macOS etc.)
140
+ VERSION_FILE="$(cd "$(dirname "$VERSION_FILE")" && pwd -P)/$(basename "$VERSION_FILE")"
141
+
142
+ local base
143
+ base="$(basename "$VERSION_FILE")"
144
+ case "$base" in
145
+ package.json) PROJECT_TYPE="npm" ;;
146
+ pyproject.toml) PROJECT_TYPE="python" ;;
147
+ Cargo.toml) PROJECT_TYPE="rust" ;;
148
+ *) die "Unrecognised manifest '${base}'. Expected: package.json | pyproject.toml | Cargo.toml" ;;
149
+ esac
150
+
151
+ local file_dir
152
+ file_dir="$(dirname "$VERSION_FILE")"
153
+ GIT_REPO_ROOT="$(git -C "$file_dir" rev-parse --show-toplevel 2>/dev/null)" || \
154
+ die "'${VERSION_FILE}' is not inside a git repository."
155
+ GIT_REPO_ROOT="$(cd "$GIT_REPO_ROOT" && pwd -P)"
156
+
157
+ info "Manifest : ${VERSION_FILE}"
158
+ info "Type : ${BOLD}${PROJECT_TYPE}${NC}"
159
+ info "Git repo root : ${GIT_REPO_ROOT}"
160
+ }
161
+
162
+ # ─────────────────────────────────────────────────────────────────────────────
163
+ # Version readers
164
+ # ─────────────────────────────────────────────────────────────────────────────
165
+ _read_npm() {
166
+ node -e "const fs=require('fs'); process.stdout.write(JSON.parse(fs.readFileSync('$1','utf8')).version)"
167
+ }
168
+
169
+ _read_python() {
170
+ python3 - "$1" <<'PY'
171
+ import sys, re
172
+ text = open(sys.argv[1]).read()
173
+ def find_in_section(pat, text):
174
+ in_sect = False
175
+ for line in text.splitlines():
176
+ s = line.strip()
177
+ if re.match(pat, s): in_sect = True; continue
178
+ if re.match(r'^\[', s): in_sect = False
179
+ if in_sect:
180
+ m = re.match(r'^version\s*=\s*["\']([^"\']+)["\']', s)
181
+ if m: return m.group(1)
182
+ return None
183
+ ver = (find_in_section(r'^\[project\]', text)
184
+ or find_in_section(r'^\[tool\.poetry\]', text))
185
+ if not ver:
186
+ m = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', text, re.M)
187
+ ver = m.group(1) if m else None
188
+ if ver: sys.stdout.write(ver); sys.exit(0)
189
+ sys.stderr.write(f"version not found in {sys.argv[1]}\n"); sys.exit(1)
190
+ PY
191
+ }
192
+
193
+ _read_rust() {
194
+ python3 - "$1" <<'PY'
195
+ import sys, re
196
+ in_pkg = False
197
+ for line in open(sys.argv[1]):
198
+ s = line.strip()
199
+ if re.match(r'^\[package\]', s): in_pkg = True; continue
200
+ if re.match(r'^\[', s): in_pkg = False
201
+ if in_pkg:
202
+ m = re.match(r'^version\s*=\s*["\']([^"\']+)["\']', s)
203
+ if m: sys.stdout.write(m.group(1)); sys.exit(0)
204
+ sys.stderr.write(f"[package] version not found in {sys.argv[1]}\n"); sys.exit(1)
205
+ PY
206
+ }
207
+
208
+ get_version() {
209
+ local file="${1:-$VERSION_FILE}"
210
+ case "$PROJECT_TYPE" in
211
+ npm) _read_npm "$file" ;;
212
+ python) _read_python "$file" ;;
213
+ rust) _read_rust "$file" ;;
214
+ esac
215
+ }
216
+
217
+ get_version_at_ref() {
218
+ local git_ref="$1"
219
+ local rel_file="${VERSION_FILE#"${GIT_REPO_ROOT}/"}"
220
+ local tmp
221
+ tmp="$(mktemp)"
222
+ trap "rm -f '${tmp}'" RETURN
223
+ git -C "$GIT_REPO_ROOT" show "${git_ref}:${rel_file}" > "$tmp" 2>/dev/null || \
224
+ die "Cannot read '${rel_file}' at ref '${git_ref}'. Is the file committed?"
225
+ get_version "$tmp"
226
+ }
227
+
228
+ # ─────────────────────────────────────────────────────────────────────────────
229
+ # Patch bumper x.y.z → x.y.(z+1)
230
+ # ─────────────────────────────────────────────────────────────────────────────
231
+ bump_patch() {
232
+ local core="${1%%[-+]*}"
233
+ local major minor patch
234
+ IFS='.' read -r major minor patch <<< "$core"
235
+ [[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ && "$patch" =~ ^[0-9]+$ ]] || \
236
+ die "Cannot parse '${1}' as semver x.y.z"
237
+ echo "${major}.${minor}.$((patch + 1))"
238
+ }
239
+
240
+ # ─────────────────────────────────────────────────────────────────────────────
241
+ # Version writers
242
+ # ─────────────────────────────────────────────────────────────────────────────
243
+ _write_npm() {
244
+ python3 - "$1" "$2" <<'PY'
245
+ import json, sys
246
+ path, ver = sys.argv[1], sys.argv[2]
247
+ d = json.load(open(path))
248
+ d['version'] = ver
249
+ with open(path, 'w') as f:
250
+ json.dump(d, f, indent=2, ensure_ascii=False); f.write('\n')
251
+ PY
252
+ }
253
+
254
+ _write_python() {
255
+ python3 - "$1" "$2" <<'PY'
256
+ import sys, re
257
+ path, new_ver = sys.argv[1], sys.argv[2]
258
+ lines = open(path).readlines()
259
+ SECTIONS = [r'^\[project\]', r'^\[tool\.poetry\]']
260
+ in_target = replaced = False
261
+ result = []
262
+ for line in lines:
263
+ s = line.strip()
264
+ if any(re.match(p, s) for p in SECTIONS): in_target = True
265
+ elif re.match(r'^\[', s): in_target = False
266
+ if in_target and not replaced:
267
+ new_line = re.sub(r'^(version\s*=\s*)["\']([^"\']+)["\']',
268
+ lambda m: m.group(1) + '"' + new_ver + '"', line)
269
+ if new_line != line: replaced = True; line = new_line
270
+ result.append(line)
271
+ if not replaced:
272
+ full = re.sub(r'^(version\s*=\s*)["\']([^"\']+)["\']',
273
+ lambda m: m.group(1) + '"' + new_ver + '"',
274
+ ''.join(result), count=1, flags=re.M)
275
+ open(path, 'w').write(full)
276
+ else:
277
+ open(path, 'w').writelines(result)
278
+ PY
279
+ }
280
+
281
+ _write_rust() {
282
+ python3 - "$1" "$2" <<'PY'
283
+ import sys, re
284
+ path, new_ver = sys.argv[1], sys.argv[2]
285
+ lines = open(path).readlines()
286
+ in_pkg = replaced = False
287
+ result = []
288
+ for line in lines:
289
+ s = line.strip()
290
+ if re.match(r'^\[package\]', s): in_pkg = True
291
+ elif re.match(r'^\[', s): in_pkg = False
292
+ if in_pkg and not replaced:
293
+ new_line = re.sub(r'^(version\s*=\s*)["\']([^"\']+)["\']',
294
+ lambda m: m.group(1) + '"' + new_ver + '"', line)
295
+ if new_line != line: replaced = True; line = new_line
296
+ result.append(line)
297
+ open(path, 'w').writelines(result)
298
+ PY
299
+ }
300
+
301
+ set_version() {
302
+ case "$PROJECT_TYPE" in
303
+ npm) _write_npm "$VERSION_FILE" "$1" ;;
304
+ python) _write_python "$VERSION_FILE" "$1" ;;
305
+ rust) _write_rust "$VERSION_FILE" "$1" ;;
306
+ esac
307
+ }
308
+
309
+ # ─────────────────────────────────────────────────────────────────────────────
310
+ # Lock file updates (best-effort, non-fatal)
311
+ # ─────────────────────────────────────────────────────────────────────────────
312
+ update_lock_files() {
313
+ case "$PROJECT_TYPE" in
314
+ rust)
315
+ if command -v cargo &>/dev/null; then
316
+ info "Regenerating Cargo.lock (cargo update --workspace)…"
317
+ cargo update --workspace --quiet 2>/dev/null || warn "cargo update failed — Cargo.lock may be stale."
318
+ else
319
+ warn "cargo not found — Cargo.lock NOT updated."
320
+ fi ;;
321
+ python)
322
+ if command -v uv &>/dev/null; then
323
+ info "Regenerating uv.lock (uv lock)…"
324
+ uv lock --quiet 2>/dev/null || warn "uv lock failed — uv.lock may be stale."
325
+ fi ;;
326
+ npm)
327
+ warn "npm lock file NOT touched — run 'npm install' locally to regenerate." ;;
328
+ esac
329
+ }
330
+
331
+ # ─────────────────────────────────────────────────────────────────────────────
332
+ # Guards
333
+ # ─────────────────────────────────────────────────────────────────────────────
334
+ validate_branch() {
335
+ step "Validating source branch"
336
+ local branch
337
+ branch="$(git -C "$GIT_REPO_ROOT" rev-parse --abbrev-ref HEAD)"
338
+
339
+ # Azure Pipelines checks out in detached HEAD state.
340
+ # In PR builds, SYSTEM_PULLREQUEST_SOURCEBRANCH gives the real source branch.
341
+ # In IndividualCI builds, BUILD_SOURCEBRANCH gives the branch.
342
+ if [[ "$branch" == "HEAD" ]]; then
343
+ local pr_branch="${SYSTEM_PULLREQUEST_SOURCEBRANCH:-}"
344
+ local ci_branch="${BUILD_SOURCEBRANCH:-${GITHUB_REF:-}}"
345
+ # Azure DevOps passes '$(Var.Name)' literally when the variable is undefined.
346
+ # Discard any value that looks like an unexpanded template.
347
+ [[ "$pr_branch" == '$('* ]] && pr_branch=""
348
+ [[ "$ci_branch" == '$('* ]] && ci_branch=""
349
+ # PR build: prefer the PR source branch (not the synthetic merge ref)
350
+ local env_branch="${pr_branch:-$ci_branch}"
351
+ branch="${env_branch#refs/heads/}"
352
+ [[ -n "$branch" ]] || \
353
+ die "Detached HEAD and no branch env var set. Cannot determine branch."
354
+ info "Detached HEAD — branch from env: ${branch}"
355
+ fi
356
+
357
+ [[ "$branch" =~ ^(feat|fix)/ ]] || \
358
+ die "Expected a feat/* or fix/* branch, currently on '${branch}'. Version bump runs on feature branches before PR merge."
359
+
360
+ ok "On feature branch '${branch}'."
361
+ CURRENT_BRANCH="$branch"
362
+ }
363
+
364
+ # Guard: compare current branch version against origin/integration.
365
+ # Same → PR did not manually bump → auto-bump this branch.
366
+ # Different → manual bump in PR → skip silently (exit 0).
367
+ validate_version_unchanged() {
368
+ step "Comparing version against origin/${INTEGRATION_BRANCH}"
369
+
370
+ local rel_file="${VERSION_FILE#"${GIT_REPO_ROOT}/"}"
371
+ local v_now v_integration
372
+
373
+ v_now="$(get_version)" || die "Cannot read current version from manifest."
374
+
375
+ # Fetch latest integration so our remote tracking ref is up to date.
376
+ git -C "$GIT_REPO_ROOT" fetch origin "${INTEGRATION_BRANCH}" --quiet 2>/dev/null || \
377
+ warn "Could not fetch origin/${INTEGRATION_BRANCH} — using cached remote tracking branch."
378
+
379
+ # If the manifest doesn't exist on integration yet (new project), just bump.
380
+ if ! git -C "$GIT_REPO_ROOT" show "origin/${INTEGRATION_BRANCH}:${rel_file}" >/dev/null 2>&1; then
381
+ warn "Manifest '${rel_file}' not found on '${INTEGRATION_BRANCH}' — treating as new project; bumping."
382
+ CURRENT_VERSION="$v_now"
383
+ return
384
+ fi
385
+
386
+ local tmp
387
+ tmp="$(mktemp)"
388
+ trap "rm -f '${tmp}'" RETURN
389
+ git -C "$GIT_REPO_ROOT" show "origin/${INTEGRATION_BRANCH}:${rel_file}" > "$tmp" 2>/dev/null
390
+ v_integration="$(get_version "$tmp")" || die "Cannot read version from origin/${INTEGRATION_BRANCH}."
391
+
392
+ info "Version on this branch : ${v_now}"
393
+ info "Version on ${INTEGRATION_BRANCH} : ${v_integration}"
394
+
395
+ if [[ "$v_now" != "$v_integration" ]]; then
396
+ printf "\n${YELLOW}${BOLD}↳ Version differs (%s here vs %s on %s). Manual bump detected — skipping auto-bump.${NC}\n\n" \
397
+ "$v_now" "$v_integration" "$INTEGRATION_BRANCH"
398
+ exit 0 # clean exit — not an error
399
+ fi
400
+
401
+ ok "Versions match at ${BOLD}${v_now}${NC} — safe to auto-bump."
402
+ CURRENT_VERSION="$v_now"
403
+ }
404
+
405
+ # ─────────────────────────────────────────────────────────────────────────────
406
+ # Commit — ***NO_CI*** suppresses Azure DevOps CI on the resulting push
407
+ # ─────────────────────────────────────────────────────────────────────────────
408
+ commit_bump() {
409
+ local new_ver="$1"
410
+ git -C "$GIT_REPO_ROOT" add "$VERSION_FILE"
411
+ case "$PROJECT_TYPE" in
412
+ rust) [[ -f "${GIT_REPO_ROOT}/Cargo.lock" ]] && git -C "$GIT_REPO_ROOT" add "${GIT_REPO_ROOT}/Cargo.lock" || true ;;
413
+ python) [[ -f "${GIT_REPO_ROOT}/uv.lock" ]] && git -C "$GIT_REPO_ROOT" add "${GIT_REPO_ROOT}/uv.lock" || true ;;
414
+ esac
415
+ git -C "$GIT_REPO_ROOT" commit -m "chore: bump version ${CURRENT_VERSION} → ${new_ver} ***NO_CI***"
416
+ ok "Committed: chore: bump version ${CURRENT_VERSION} → ${new_ver} ***NO_CI***"
417
+ }
418
+
419
+ push_bump() {
420
+ local remote
421
+ remote="$(git -C "$GIT_REPO_ROOT" remote | head -1)"
422
+ [[ -n "$remote" ]] || die "No git remote found — cannot push."
423
+ info "Pushing to ${remote}/${CURRENT_BRANCH}…"
424
+ git -C "$GIT_REPO_ROOT" push "$remote" "HEAD:${CURRENT_BRANCH}"
425
+ ok "Pushed to ${remote}/${CURRENT_BRANCH}."
426
+ }
427
+
428
+ # ─────────────────────────────────────────────────────────────────────────────
429
+ # Main
430
+ # ─────────────────────────────────────────────────────────────────────────────
431
+ main() {
432
+ parse_args "$@"
433
+ resolve_manifest
434
+ validate_branch
435
+ validate_version_unchanged # exits 0 silently if version was manually bumped
436
+
437
+ local new_version
438
+ new_version="$(bump_patch "$CURRENT_VERSION")"
439
+
440
+ step "Version bump"
441
+ info " ${CURRENT_VERSION} → ${BOLD}${new_version}${NC}"
442
+
443
+ if $DRY_RUN; then
444
+ warn "Dry-run — no files modified, no commit made."
445
+ return 0
446
+ fi
447
+
448
+ set_version "$new_version"
449
+ ok "Updated ${VERSION_FILE}"
450
+ update_lock_files "$new_version"
451
+
452
+ step "Committing"
453
+ commit_bump "$new_version"
454
+
455
+ if $DO_PUSH; then
456
+ step "Pushing"
457
+ push_bump
458
+ fi
459
+
460
+ printf "\n${GREEN}${BOLD}✓ Done.${NC} Version bumped to ${BOLD}%s${NC}\n\n" "$new_version"
461
+ }
462
+
463
+ main "$@"
package/spec/codex.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "codex-plugin.json",
4
4
  "title": "Codex Plugin Manifest",
5
- "description": "Validates .codex-plugin/plugin.json for OpenAI Codex plugins. Matches the enact-operator plugin structure.",
5
+ "description": "Validates .codex-plugin/plugin.json for OpenAI Codex plugins. Based on the OpenAI Codex plugin build docs.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "name"
@@ -69,11 +69,6 @@
69
69
  "description": "Relative path to the skills directory.",
70
70
  "default": "./skills/"
71
71
  },
72
- "commands": {
73
- "type": "string",
74
- "description": "Relative path to slash commands directory (flat Markdown files).",
75
- "default": "./commands/"
76
- },
77
72
  "mcpServers": {
78
73
  "type": "string",
79
74
  "description": "Relative path to the .mcp.json MCP server configuration file.",
@@ -144,11 +139,6 @@
144
139
  "description": "Human-readable list of capabilities shown in the plugin directory."
145
140
  }
146
141
  }
147
- },
148
- "agents": {
149
- "type": "string",
150
- "description": "Relative path to the agents dir.",
151
- "default": "./agents/"
152
142
  }
153
143
  }
154
144
  }