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 +18 -0
- package/README.md +9 -2
- package/bin/agent-orchestrator.js +134 -21
- package/package.json +2 -1
- package/profiles/generic/orchestrator.yaml +3 -0
- package/profiles/mvp/orchestrator.yaml +3 -0
- package/profiles/node/orchestrator.yaml +3 -0
- package/profiles/vue3/orchestrator.yaml +3 -0
- package/templates/orchestrator.yaml +3 -0
- package/templates/scripts/sync-local-agent-skills.sh +27 -0
- package/templates/.cursor/memory.json +0 -11
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
4024
|
-
: gitDiffTouchesGlob(projectDir, opts.base,
|
|
4135
|
+
? gitStagedTouchesGlob(projectDir, srcGlob)
|
|
4136
|
+
: gitDiffTouchesGlob(projectDir, opts.base, srcGlob);
|
|
4025
4137
|
if (touchesSrc === false) {
|
|
4026
|
-
log.ok(`no ${opts.staged ? 'staged ' : ''}changes under ${
|
|
4138
|
+
log.ok(`no ${opts.staged ? 'staged ' : ''}changes under ${srcGlob} — nothing to gate`);
|
|
4027
4139
|
return;
|
|
4028
4140
|
}
|
|
4029
4141
|
if (touchesSrc === null) {
|
|
4030
|
-
|
|
4031
|
-
|
|
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(`${
|
|
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.
|
|
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"}
|