agent-orchestrator-kit 0.14.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Fixed
8
+ - **Cache-split cost estimates survive the session recompute.** `applyCollectedSessionFields` recomputed `byModel[].costUsdEstimated` from `inputTokens` + `outputTokens` alone and overwrote the value the adapters had already produced. Since `inputTokens` includes `cache_read_*` / `cache_creation_*`, every cached token was billed at the full input rate: a real Claude change with 3.5M tokens estimated `$30.11` instead of `$5.16`, and Cursor rows lost their own rate table to the Claude `$3/$15` fallback. The recompute now only fills rows that have no adapter estimate, so ESTIMATE-ALL still covers Amp rows without `Cost:` while `claude-opus-5` with 100k input + 900k cache read + 10k output stays at `$1.20`.
9
+
10
+ ## [0.14.1] - 2026-09-08
11
+
12
+ ### Fixed
13
+ - **Memory graph survives `handoff` persist.** `loadMemoryItems` classified a memory file by its first character, so every JSONL graph (the format `@modelcontextprotocol/server-memory` reads and writes) was treated as an aggregate `{entities, relations}` document, failed to parse, and returned `[]` — the next persist then overwrote the whole file with just the current change's two entities. Everything the Memory MCP server had accumulated — all `Decision:*`, every other change, and all relations — was destroyed on each persist, leaving cross-session memory permanently one change deep. Detection is now by shape, `handoff --restore` reads JSONL graphs again instead of reporting "Memory JSON empty or missing", and a line that cannot be parsed is preserved verbatim rather than dropped.
14
+ - **`/opsx:*` commands reach the IDEs.** `.agents/commands/` was installed and documented but synced nowhere. `sync` now writes `.cursor/commands/opsx-<phase>.md` (flat, `/opsx-<phase>`) and `.claude/commands/opsx/<phase>.md` (namespaced, so the documented `/opsx:<phase>` exists in Claude Code), with the same stale-file deletion as skills and subagents. `sync-local-agent-skills.sh` matches.
15
+ - **`status` readiness mirrors the archive gates.** "ready to archive" was computed from `tasks.md` checkboxes alone, so it appeared on a change `archive` would refuse for a missing or non-APPROVE verdict. It now applies `require_spec_review` / `require_design_brief` and names the blockers.
16
+ - **`gate-check` no longer passes silently.** When the git diff cannot be computed (shallow clone, missing base ref, no commits) the review gate is verified instead of skipped.
17
+ - The next-thread prompt renders `- Change: <name>` instead of `- Change: - name: <name>`.
18
+
19
+ ### Removed
20
+ - `templates/.cursor/memory.json` — an unused snapshot of the kit's own development memory. It was referenced by nothing (`init` writes an empty `.cursor/memory.json`), gitignored so it never showed up in a diff, yet the `files` allowlist still published it to npm. `files` now excludes `templates/.cursor` so it cannot ship again.
21
+
22
+ ### Added
23
+ - `pipeline.src_glob` in `orchestrator.yaml` — the paths `gate-check` treats as product code. The hardcoded `src/` default silently disabled the review gate in repos whose code lives elsewhere; `--src-glob` still overrides.
24
+
7
25
  ## [0.14.0] - 2026-09-07
8
26
 
9
27
  ### Changed
package/README.md CHANGED
@@ -74,7 +74,7 @@ npx agent-orchestrator-kit@latest init --profile generic --ci gitlab --spec-veri
74
74
 
75
75
  See [Installation](#installation) for profile/CI options.
76
76
 
77
- **🔄 Already have the kit installed? Upgrade to latest (compact metrics schema v2, exhaustive Tier 2 punch list, bounded collection windows, Claude/Amp estimates, and dashboard JSON in v0.14.0):**
77
+ **🔄 Already have the kit installed? Upgrade to latest (v0.14.1 fixes a `handoff` persist that wiped the Memory graph, syncs the `/opsx:*` commands into Cursor and Claude Code, and stops `status` / `gate-check` from reporting a gate as met when it is not):**
78
78
 
79
79
  ```bash
80
80
  npx agent-orchestrator-kit@latest update
@@ -189,7 +189,7 @@ your-project/
189
189
  |----------|----------|
190
190
  | Orchestration | 5-role pipeline, `AGENTS.md`, `orchestrator.yaml`, review command |
191
191
  | OpenSpec skills | All 7 skills for `/opsx:*` workflow |
192
- | IDE sync | Cursor + Claude Code sync script (`--delete` semantics — removes stale skills/subagents) |
192
+ | IDE sync | Cursor + Claude Code sync script (`--delete` semantics — removes stale skills/subagents/commands) |
193
193
  | Subagents | 12 exclusive routes: guide/setup/session-handoff, explore/design/propose/review/archive stage agents, and apply implementation/test/code-review agents — native in Cursor + Claude Code, isolated Amp `subagent-*` wrappers |
194
194
  | CLI gates | `npx agent-orchestrator-kit status` / `gate-check` / `archive` / `handoff` / `metrics` / `memory-setup` — deterministic review-gate, archive, session-handoff, and change metrics (always via `npx`; see `cli-via-npm.mdc`) |
195
195
  | CI | `agent-verify.yml` — GitHub (default) or GitLab fragment + `prebuild` hook, both run `gate-check` |
@@ -986,11 +986,13 @@ npx agent-orchestrator-kit metrics [change-name] [--json] [--collect]
986
986
  skills/ # Synced from .agents/skills/
987
987
  rules/ # Synced from .agents/rules/
988
988
  agents/ # Synced from .agents/subagents/
989
+ commands/ # Synced from .agents/commands/ (flat — /opsx-apply)
989
990
  memory.json # Memory MCP data
990
991
 
991
992
  .claude/ # Local only — Claude Code runtime
992
993
  skills/ # Synced from .agents/skills/
993
994
  agents/ # Synced from .agents/subagents/
995
+ commands/opsx/ # Synced from .agents/commands/ (namespaced — /opsx:apply)
994
996
  CLAUDE.md # Synced from root CLAUDE.md
995
997
 
996
998
  .amp/ # Local only — Amp config
@@ -1015,6 +1017,11 @@ Phase bounds and non-goals: [`openspec/specs/agentic-factory-roadmap/spec.md`](o
1015
1017
 
1016
1018
  ## Changelog
1017
1019
 
1020
+ ### 0.14.1
1021
+ - **Memory graph survives `handoff` persist** — JSONL graphs were misread as an aggregate document and returned empty, so each persist overwrote the file with only the current change; all `Decision:*`, other changes, and relations were lost
1022
+ - `/opsx:*` commands now sync to `.cursor/commands/` (flat) and `.claude/commands/opsx/` (namespaced), so the documented slash commands exist in both IDEs
1023
+ - `status` applies `require_spec_review` / `require_design_brief` before saying "ready to archive"; `gate-check` verifies the gate instead of exiting 0 when the diff is unknown, and reads `pipeline.src_glob` for code outside `src/`
1024
+
1018
1025
  ### 0.14.0
1019
1026
  - **BREAKING: compact metrics schema v2** — sessions persist `sourceIds` / `sourceTotals` / `byModel` instead of `sources`; `metrics --migrate` is schema-only; `metrics --summary-json` for dashboards
1020
1027
  - Bounded persist/leftover windows, thread-scoped Amp usage + fresh Cost, Claude dedup/subagent capture, static Claude/Amp estimates
@@ -220,6 +220,47 @@ function copyDir(src, dest, opts = {}) {
220
220
  }
221
221
  }
222
222
 
223
+ // `.agents/commands/opsx-<phase>.md` is the source of truth for the /opsx:*
224
+ // role commands. Cursor reads `.cursor/commands/<file>.md` as `/<file>`, so it
225
+ // gets a flat copy. Claude Code namespaces by subdirectory —
226
+ // `.claude/commands/opsx/<phase>.md` is what makes the documented
227
+ // `/opsx:<phase>` actually exist there.
228
+ function syncCommands(projectDir, ideDir, { namespaced }) {
229
+ const src = join(projectDir, '.agents', 'commands');
230
+ const dest = join(projectDir, ideDir, 'commands');
231
+ if (!existsSync(src)) return;
232
+
233
+ const files = readdirSync(src).filter((f) => f.endsWith('.md'));
234
+ const written = new Set();
235
+ mkdirSync(dest, { recursive: true });
236
+
237
+ for (const file of files) {
238
+ const match = namespaced && file.match(/^([a-z0-9]+)-(.+\.md)$/i);
239
+ const rel = match ? join(match[1], match[2]) : file;
240
+ const destPath = join(dest, rel);
241
+ mkdirSync(dirname(destPath), { recursive: true });
242
+ copyFileSync(join(src, file), destPath);
243
+ written.add(rel);
244
+ log.ok(destPath.replace(process.cwd() + '/', ''));
245
+ }
246
+
247
+ // Drop commands a kit `update` removed, mirroring the skills/subagents sync.
248
+ const walk = (dir, prefix = '') => {
249
+ for (const entry of readdirSync(dir)) {
250
+ const full = join(dir, entry);
251
+ const rel = prefix ? join(prefix, entry) : entry;
252
+ if (statSync(full).isDirectory()) {
253
+ walk(full, rel);
254
+ if (readdirSync(full).length === 0) rmSync(full, { recursive: true, force: true });
255
+ } else if (!written.has(rel)) {
256
+ rmSync(full, { force: true });
257
+ log.warn(`removed stale: ${join(dest, rel).replace(process.cwd() + '/', '')}`);
258
+ }
259
+ }
260
+ };
261
+ walk(dest);
262
+ }
263
+
223
264
  function gitignoreLines(content) {
224
265
  return content.split('\n').map((l) => l.trim()).filter(Boolean);
225
266
  }
@@ -960,6 +1001,26 @@ function parseHandoffMarkdown(content) {
960
1001
  return sections;
961
1002
  }
962
1003
 
1004
+ // The `## Change` section is a bullet list (`- name: <x>`), but the next-thread
1005
+ // prompt inlines it after its own bullet. Flatten it so the prompt does not
1006
+ // read "- Change: - name: <x>".
1007
+ function inlineChangeLabel(value, fallbackName) {
1008
+ const text = String(value || '').trim();
1009
+ if (!text) return fallbackName;
1010
+ const parts = text
1011
+ .split('\n')
1012
+ .map((line) => line.replace(/^\s*[-*]\s*/, '').trim())
1013
+ .filter(Boolean);
1014
+ if (parts.length === 0) return fallbackName;
1015
+ const named = parts.find((p) => /^name:\s*/i.test(p));
1016
+ if (named) {
1017
+ const rest = parts.filter((p) => p !== named);
1018
+ const label = named.replace(/^name:\s*/i, '').trim() || fallbackName;
1019
+ return rest.length ? `${label} (${rest.join('; ')})` : label;
1020
+ }
1021
+ return parts.join('; ');
1022
+ }
1023
+
963
1024
  function firstLineCommand(value) {
964
1025
  const match = String(value || '').match(/\/opsx:[^\s`]+(?:\s+[^\s`]+)?/);
965
1026
  if (match) return match[0].trim();
@@ -1397,7 +1458,7 @@ function buildNextSessionPrompt(fields, agentLanguage) {
1397
1458
 
1398
1459
  ## Повний контекст попередньої сесії (самодостатній — не покладайся лише на Memory)
1399
1460
  - Закрита роль: ${fields.closedRole || 'не вказано'}
1400
- - Зміна: ${fields.change || name}
1461
+ - Зміна: ${inlineChangeLabel(fields.change, name)}
1401
1462
  - Зроблено:
1402
1463
  ${fields.done || 'не вказано'}
1403
1464
  - Рішення:
@@ -1449,7 +1510,7 @@ Do not mix phases. Do not start the following role in this chat until this phase
1449
1510
 
1450
1511
  ## Full previous-session context (self-contained — do not rely on Memory alone)
1451
1512
  - Closed role: ${fields.closedRole || 'not set'}
1452
- - Change: ${fields.change || name}
1513
+ - Change: ${inlineChangeLabel(fields.change, name)}
1453
1514
  - Done:
1454
1515
  ${fields.done || 'not set'}
1455
1516
  - Decisions:
@@ -1481,26 +1542,48 @@ function loadMemoryItems(filePath) {
1481
1542
  if (!existsSync(filePath)) return [];
1482
1543
  const raw = readFileSync(filePath, 'utf-8').trim();
1483
1544
  if (!raw) return [];
1484
- if (raw.startsWith('{')) {
1485
- try {
1486
- const parsed = JSON.parse(raw);
1545
+
1546
+ // Aggregate form is a single JSON document: { entities: [...], relations: [...] }.
1547
+ // Every JSONL line is an object too, so the *shape* decides — not the first
1548
+ // character. Keying off `{` classified every JSONL graph as an aggregate,
1549
+ // parsed it as one document, failed, and returned [] — which made the next
1550
+ // persist overwrite the whole graph with just the current change.
1551
+ try {
1552
+ const parsed = JSON.parse(raw);
1553
+ if (parsed && !Array.isArray(parsed) && (Array.isArray(parsed.entities) || Array.isArray(parsed.relations))) {
1487
1554
  const entities = (parsed.entities || []).map((entity) => ({ type: 'entity', ...entity }));
1488
1555
  const relations = (parsed.relations || []).map((relation) => ({ type: 'relation', ...relation }));
1489
1556
  return [...entities, ...relations];
1557
+ }
1558
+ } catch {
1559
+ // Not a single JSON document — fall through to JSONL.
1560
+ }
1561
+
1562
+ // JSONL: the format @modelcontextprotocol/server-memory reads and writes,
1563
+ // one entity or relation per line. A line we cannot parse is kept verbatim
1564
+ // so a persist never drops memory it failed to understand.
1565
+ const items = [];
1566
+ for (const line of raw.split('\n')) {
1567
+ const trimmed = line.trim();
1568
+ if (!trimmed) continue;
1569
+ let parsed;
1570
+ try {
1571
+ parsed = JSON.parse(trimmed);
1490
1572
  } catch {
1491
- return [];
1573
+ items.push({ __raw: trimmed });
1574
+ continue;
1492
1575
  }
1576
+ if (parsed && typeof parsed === 'object' && parsed.type) items.push(parsed);
1577
+ else items.push({ __raw: trimmed });
1493
1578
  }
1494
- return raw
1495
- .split('\n')
1496
- .map((line) => line.trim())
1497
- .filter(Boolean)
1498
- .map((line) => JSON.parse(line));
1579
+ return items;
1499
1580
  }
1500
1581
 
1501
1582
  function saveMemoryItems(filePath, items) {
1502
1583
  mkdirSync(dirname(filePath), { recursive: true });
1503
- const body = items.map((item) => JSON.stringify(item)).join('\n');
1584
+ const body = items
1585
+ .map((item) => (item && item.__raw !== undefined ? item.__raw : JSON.stringify(item)))
1586
+ .join('\n');
1504
1587
  writeFileSync(filePath, body ? `${body}\n` : '');
1505
1588
  }
1506
1589
 
@@ -2383,6 +2466,9 @@ function applyCollectedSessionFields(session, sources, resolvedModel, opts, repo
2383
2466
  if (session.costUsd == null) {
2384
2467
  for (const row of session.byModel) {
2385
2468
  if (row.costUsd != null) continue;
2469
+ // adapters already estimated with the cache-read / cache-write split; do not
2470
+ // overwrite it with the coarse input+output estimate below
2471
+ if (row.costUsdEstimated != null) continue;
2386
2472
  const described = describeClaudeCostEstimate({
2387
2473
  model: String(row.model || '').replace(/^Claude\s+/i, 'claude-').replaceAll(' ', '-').toLowerCase(),
2388
2474
  inputTokens: row.inputTokens,
@@ -3055,11 +3141,13 @@ function readPipelineConfig(projectDir) {
3055
3141
  const requireBriefMatch = content.match(/require_design_brief:\s*(true|false)/);
3056
3142
  const maxActiveMatch = content.match(/max_active_changes:\s*(\d+)/);
3057
3143
  const taskContractMatch = content.match(/task_contract:\s*(warn|strict|off)/);
3144
+ const srcGlobMatch = content.match(/src_glob:\s*["']?([^"'\s#]+)["']?/);
3058
3145
  return {
3059
3146
  requireSpecReview: requireReviewMatch ? requireReviewMatch[1] === 'true' : true,
3060
3147
  requireDesignBrief: requireBriefMatch ? requireBriefMatch[1] === 'true' : false,
3061
3148
  maxActiveChanges: maxActiveMatch ? parseInt(maxActiveMatch[1], 10) : null,
3062
3149
  taskContract: taskContractMatch ? taskContractMatch[1] : 'warn',
3150
+ srcGlob: srcGlobMatch ? srcGlobMatch[1] : null,
3063
3151
  };
3064
3152
  }
3065
3153
 
@@ -3891,6 +3979,7 @@ program
3891
3979
  }
3892
3980
  copyDir(join(projectDir, '.agents', 'rules'), join(projectDir, '.cursor', 'rules'), { overwrite: true, delete: true });
3893
3981
  copyDir(join(projectDir, '.agents', 'subagents'), join(projectDir, '.cursor', 'agents'), { overwrite: true, delete: true });
3982
+ syncCommands(projectDir, '.cursor', { namespaced: false });
3894
3983
  }
3895
3984
 
3896
3985
  if (syncClaude) {
@@ -3900,6 +3989,7 @@ program
3900
3989
  rmSync(join(projectDir, '.claude', 'skills', wrapper), { recursive: true, force: true });
3901
3990
  }
3902
3991
  copyDir(join(projectDir, '.agents', 'subagents'), join(projectDir, '.claude', 'agents'), { overwrite: true, delete: true });
3992
+ syncCommands(projectDir, '.claude', { namespaced: true });
3903
3993
 
3904
3994
  const claudeMd = join(projectDir, 'CLAUDE.md');
3905
3995
  const claudeDir = join(projectDir, '.claude');
@@ -3937,6 +4027,13 @@ program
3937
4027
  if (changes.length === 0) {
3938
4028
  log.info('No active changes');
3939
4029
  } else {
4030
+ // Readiness must mirror the gates `archive` actually enforces — reporting
4031
+ // "ready" on task count alone told the conductor to archive a change the
4032
+ // CLI would then refuse.
4033
+ const config = readPipelineConfig(projectDir);
4034
+ const requireReview = config ? config.requireSpecReview : true;
4035
+ const requireBrief = config ? config.requireDesignBrief : false;
4036
+
3940
4037
  for (const name of changes) {
3941
4038
  const changeDir = join(projectDir, 'openspec', 'changes', name);
3942
4039
  const progress = parseTasksProgress(changeDir);
@@ -3944,13 +4041,24 @@ program
3944
4041
  const hasBrief = parseDesignBrief(changeDir);
3945
4042
  const progressStr = progress ? `${progress.done}/${progress.total} tasks` : 'no tasks.md';
3946
4043
  const verdictStr = verdict || 'none';
3947
- const readyToArchive = Boolean(progress && progress.total > 0 && progress.done === progress.total);
4044
+
4045
+ const blockers = [];
4046
+ if (!(progress && progress.total > 0 && progress.done === progress.total)) {
4047
+ blockers.push('tasks incomplete');
4048
+ }
4049
+ if (requireReview && !(verdict && /^APPROVE/i.test(verdict))) {
4050
+ blockers.push(verdict ? `review verdict "${verdict}" (need APPROVE)` : 'no review.md');
4051
+ }
4052
+ if (requireBrief && !hasBrief && !hasDesignOptOut(changeDir)) {
4053
+ blockers.push('no design-brief.md');
4054
+ }
3948
4055
 
3949
4056
  console.log(`\n${pc.bold(name)}`);
3950
4057
  console.log(` tasks: ${progressStr}`);
3951
4058
  console.log(` review: ${verdictStr}`);
3952
4059
  console.log(` brief: ${hasBrief ? 'yes' : 'no'}`);
3953
- if (readyToArchive) log.ok('ready to archive');
4060
+ if (blockers.length === 0) log.ok('ready to archive');
4061
+ else log.info(`not ready to archive — ${blockers.join('; ')}`);
3954
4062
  }
3955
4063
  console.log('');
3956
4064
  }
@@ -3963,7 +4071,7 @@ program
3963
4071
  program
3964
4072
  .command('gate-check [change-name]')
3965
4073
  .description('Deterministically check the review gate before apply/merge (exit non-zero if unmet)')
3966
- .option('--src-glob <glob>', 'source path filter used to detect code changes', 'src/')
4074
+ .option('--src-glob <glob>', 'source path filter used to detect code changes (default: pipeline.src_glob, else src/)')
3967
4075
  .option('--base <ref>', 'git ref to diff against', 'HEAD~1')
3968
4076
  .option('--staged', 'check staged files (git diff --cached) instead of --base...HEAD', false)
3969
4077
  .option('--tasks <name>', 'lint task contracts (Files/Do/Done-when) of a change')
@@ -4019,16 +4127,21 @@ program
4019
4127
  return;
4020
4128
  }
4021
4129
 
4130
+ // A repo whose code does not live in src/ used to fall through to
4131
+ // "nothing to gate" on every run, silently disabling the review gate.
4132
+ const srcGlob = opts.srcGlob || config.srcGlob || 'src/';
4133
+
4022
4134
  const touchesSrc = opts.staged
4023
- ? gitStagedTouchesGlob(projectDir, opts.srcGlob)
4024
- : gitDiffTouchesGlob(projectDir, opts.base, opts.srcGlob);
4135
+ ? gitStagedTouchesGlob(projectDir, srcGlob)
4136
+ : gitDiffTouchesGlob(projectDir, opts.base, srcGlob);
4025
4137
  if (touchesSrc === false) {
4026
- log.ok(`no ${opts.staged ? 'staged ' : ''}changes under ${opts.srcGlob} — nothing to gate`);
4138
+ log.ok(`no ${opts.staged ? 'staged ' : ''}changes under ${srcGlob} — nothing to gate`);
4027
4139
  return;
4028
4140
  }
4029
4141
  if (touchesSrc === null) {
4030
- log.warn(`could not compute git ${opts.staged ? 'staged ' : ''}diff — skipping gate-check`);
4031
- return;
4142
+ // Cannot prove nothing changed (shallow clone, missing base ref, no
4143
+ // commits yet) — verify the gate instead of passing a blocking check.
4144
+ log.warn(`could not compute git ${opts.staged ? 'staged ' : ''}diff — verifying the review gate anyway`);
4032
4145
  }
4033
4146
 
4034
4147
  const changes = listActiveChanges(projectDir);
@@ -4039,7 +4152,7 @@ program
4039
4152
  let target = changeName;
4040
4153
  if (!target) {
4041
4154
  if (changes.length === 0) {
4042
- log.warn(`${opts.srcGlob} changed but no active OpenSpec change found — cannot verify review gate`);
4155
+ log.warn(`${srcGlob} changed but no active OpenSpec change found — cannot verify review gate`);
4043
4156
  return;
4044
4157
  }
4045
4158
  target = changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-kit",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, durable session handoff, factory gates and MCP setup, cloud-agent handoff, and optional local Figma PAT setup",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -35,6 +35,7 @@
35
35
  "files": [
36
36
  "bin/",
37
37
  "templates/",
38
+ "!templates/.cursor",
38
39
  "profiles/",
39
40
  "README.md",
40
41
  "CHANGELOG.md",
@@ -11,6 +11,9 @@ pipeline:
11
11
  max_active_changes: 1
12
12
  archive_after_merge: true
13
13
  task_contract: warn
14
+ # Paths gate-check treats as product code. Widen it when code lives
15
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
16
+ src_glob: "src/"
14
17
 
15
18
  roles:
16
19
  explorer:
@@ -16,6 +16,9 @@ pipeline:
16
16
  archive_after_merge: false
17
17
  quick_mode_enabled: true
18
18
  task_contract: off
19
+ # Paths gate-check treats as product code. Widen it when code lives
20
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
21
+ src_glob: "src/"
19
22
 
20
23
  roles:
21
24
  explorer:
@@ -14,6 +14,9 @@ pipeline:
14
14
  max_active_changes: 1
15
15
  archive_after_merge: true
16
16
  task_contract: warn
17
+ # Paths gate-check treats as product code. Widen it when code lives
18
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
19
+ src_glob: "src/"
17
20
 
18
21
  roles:
19
22
  explorer:
@@ -14,6 +14,9 @@ pipeline:
14
14
  max_active_changes: 1
15
15
  archive_after_merge: true
16
16
  task_contract: warn
17
+ # Paths gate-check treats as product code. Widen it when code lives
18
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
19
+ src_glob: "src/"
17
20
 
18
21
  roles:
19
22
  explorer:
@@ -13,6 +13,9 @@ pipeline:
13
13
  max_active_changes: 1
14
14
  archive_after_merge: true
15
15
  task_contract: warn
16
+ # Paths gate-check treats as product code. Widen it when code lives
17
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
18
+ src_glob: "src/"
16
19
 
17
20
  roles:
18
21
  explorer:
@@ -76,6 +76,13 @@ if [ -d .agents/subagents ]; then
76
76
  ok ".cursor/agents/"
77
77
  fi
78
78
 
79
+ # Cursor reads .cursor/commands/<file>.md as /<file> — flat copy.
80
+ if [ -d .agents/commands ]; then
81
+ mkdir -p .cursor/commands
82
+ rsync -a --delete .agents/commands/ .cursor/commands/
83
+ ok ".cursor/commands/"
84
+ fi
85
+
79
86
  if [ ! -f .mcp.json ] && [ -f .agents/mcp.json.example ]; then
80
87
  cp .agents/mcp.json.example .mcp.json
81
88
  ok ".mcp.json created from example"
@@ -97,6 +104,26 @@ if [ -d .agents/subagents ]; then
97
104
  ok ".claude/agents/"
98
105
  fi
99
106
 
107
+ # Claude Code namespaces commands by subdirectory: .claude/commands/opsx/apply.md
108
+ # is what makes the documented /opsx:apply exist.
109
+ if [ -d .agents/commands ]; then
110
+ rm -rf .claude/commands
111
+ mkdir -p .claude/commands
112
+ for cmd in .agents/commands/*.md; do
113
+ [ -f "$cmd" ] || continue
114
+ BASE="$(basename "$cmd" .md)"
115
+ NS="${BASE%%-*}"
116
+ REST="${BASE#*-}"
117
+ if [ "$NS" != "$BASE" ] && [ -n "$REST" ]; then
118
+ mkdir -p ".claude/commands/$NS"
119
+ cp "$cmd" ".claude/commands/$NS/$REST.md"
120
+ else
121
+ cp "$cmd" ".claude/commands/$BASE.md"
122
+ fi
123
+ done
124
+ ok ".claude/commands/"
125
+ fi
126
+
100
127
  if [ -f CLAUDE.md ]; then
101
128
  cp CLAUDE.md .claude/CLAUDE.md
102
129
  ok ".claude/CLAUDE.md"
@@ -1,11 +0,0 @@
1
- {"type":"entity","name":"Change:add-factory-memory-and-skills","entityType":"Change","observations":["status: archived","tasks: 9/9","last_role: Archiver","review: APPROVE","summary: archived to openspec/changes/archive/2026-08-27-add-factory-memory-and-skills"]}
2
- {"type":"entity","name":"Handoff:add-factory-memory-and-skills","entityType":"Handoff","observations":["next_role: none","next_command: none","summary: archived to openspec/changes/archive/2026-08-27-add-factory-memory-and-skills","blocked: none"]}
3
- {"type":"entity","name":"Decision:apply-complete","entityType":"Decision","observations":["all 9 tasks implemented and verified (npm test 101/101, openspec validate --strict, gate-check --tasks)","change: add-factory-memory-and-skills","date: 2026-08-27"]}
4
- {"type":"entity","name":"Decision:m1-followed","entityType":"Decision","observations":["task 2.3 Skill health stale/missing verified in a temporary init+sync project (smoke test), not in the kit repo","change: add-factory-memory-and-skills","date: 2026-08-27"]}
5
- {"type":"entity","name":"Decision:m2-followed","entityType":"Decision","observations":["skills.kit drift test iterates all five orchestrator.yaml files against templates/.agents/skills/","change: add-factory-memory-and-skills","date: 2026-08-27"]}
6
- {"type":"entity","name":"Decision:i1-honored","entityType":"Decision","observations":["did not change templates/orchestrator.yaml handoff.spawn_handoff_subagent (still false)","change: add-factory-memory-and-skills","date: 2026-08-27"]}
7
- {"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Handoff:add-factory-memory-and-skills","relationType":"hasHandoff"}
8
- {"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:apply-complete","relationType":"hasDecision"}
9
- {"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:m1-followed","relationType":"hasDecision"}
10
- {"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:m2-followed","relationType":"hasDecision"}
11
- {"type":"relation","from":"Change:add-factory-memory-and-skills","to":"Decision:i1-honored","relationType":"hasDecision"}