agent-orchestrator-kit 0.2.0 → 0.3.0
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 +22 -0
- package/README.md +50 -16
- package/bin/agent-orchestrator.js +405 -3
- package/package.json +1 -1
- package/profiles/generic/orchestrator.yaml +2 -1
- package/profiles/mvp/openspec-config.yaml.example +2 -0
- package/profiles/mvp/orchestrator.yaml +2 -1
- package/profiles/node/orchestrator.yaml +2 -1
- package/profiles/vue3/openspec-config.yaml.example +2 -0
- package/profiles/vue3/orchestrator.yaml +2 -1
- package/templates/.agents/commands/opsx-apply.md +18 -46
- package/templates/.agents/commands/opsx-archive.md +10 -163
- package/templates/.agents/commands/opsx-design.md +3 -10
- package/templates/.agents/commands/opsx-explore.md +3 -10
- package/templates/.agents/commands/opsx-propose.md +14 -10
- package/templates/.agents/commands/opsx-quick.md +3 -10
- package/templates/.agents/commands/opsx-review.md +27 -55
- package/templates/.agents/commands/opsx-sync.md +2 -0
- package/templates/.agents/rules/agent-orchestration.mdc +15 -16
- package/templates/.agents/rules/session-handoff.mdc +18 -16
- package/templates/.agents/skills/agent-orchestration/SKILL.md +27 -28
- package/templates/.agents/skills/openspec-apply-change/SKILL.md +15 -20
- package/templates/.agents/skills/openspec-archive-change/SKILL.md +12 -99
- package/templates/.agents/skills/openspec-propose/SKILL.md +11 -0
- package/templates/.agents/subagents/session-handoff.md +5 -5
- package/templates/.agents/subagents/spec-architect.md +1 -1
- package/templates/.agents/subagents/spec-archiver.md +1 -1
- package/templates/.agents/subagents/spec-reviewer.md +11 -7
- package/templates/AGENTS.md +3 -3
- package/templates/CLAUDE.md +2 -2
- package/templates/orchestrator.yaml +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-08-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`npx agent-orchestrator-kit archive <name>`** — deterministic archive CLI: gates (APPROVE, all tasks `[x]`, free target), delta-spec merge into main specs with `--sync` (ADDED append / MODIFIED replace / REMOVED delete), explicit refusal without a sync decision (`--sync` or `--no-sync --force`), snapshot + full rollback when `openspec validate --all --strict` fails, final `handoff.md` (`next_command: none`) + memory upsert
|
|
11
|
+
- **Task contract lint** — `gate-check --tasks <name>` enforces `Files:` / `Do:` / `Done-when:` per task, rejects vague phrasing and nonexistent `Files:` paths without `new file:`; controlled by `pipeline.task_contract: warn|strict|off` (default `warn`, mvp `off`)
|
|
12
|
+
- **Tiered review** — `gate-check --review <name> [--json]` is deterministic Tier 1 (strict validation, task-contract lint, proposal `Non-goals`/`Acceptance criteria`, non-empty delta sections); `spec-reviewer` runs only after Tier 1 passes and writes `apply-notes.md` (≤ 20 lines) on APPROVE
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Lean delegation model** — apply is parent-driven (implementer reads `tasks.md` + `apply-notes.md`, spawns subagents only for ≥ 2 independent tasks or on request, STOP escape valve instead of improvisation); archive is CLI-only with `spec-archiver` demoted to fallback; propose/review specialists remain mandatory
|
|
16
|
+
- **Parent-driven session handoff** — canonical Session Start/Exit protocol consolidated in `.agents/rules/session-handoff.mdc`; `/opsx:*` commands reference it instead of duplicating it; `session-handoff` subagent and Memory MCP mirror are fallbacks (`handoff.spawn_handoff_subagent: false` in all profiles); `opsx-archive.md` slimmed to a ≤ 1.5 KB CLI wrapper
|
|
17
|
+
|
|
18
|
+
## [0.2.0] - 2026-08-18
|
|
19
|
+
|
|
7
20
|
### Changed
|
|
8
21
|
- **Context budget** — thinned always-apply rules and `AGENTS.md` / `CLAUDE.md`; details stay in on-demand skills. `figma-token-setup.mdc` is no longer `alwaysApply`. Restore spawn of `session-handoff` is skipped when `handoff --restore` already printed a briefing.
|
|
9
22
|
|
|
@@ -168,8 +181,17 @@ All notable changes to this project will be documented in this file.
|
|
|
168
181
|
### Added
|
|
169
182
|
- Initial release: 5-role orchestration pipeline, `/opsx:*` commands, IDE sync
|
|
170
183
|
|
|
184
|
+
[0.3.0]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.2.0...v0.3.0
|
|
185
|
+
[0.2.0]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.14...v0.2.0
|
|
186
|
+
[0.1.14]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.13...v0.1.14
|
|
187
|
+
[0.1.13]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.12...v0.1.13
|
|
188
|
+
[0.1.12]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.11...v0.1.12
|
|
189
|
+
[0.1.11]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.10...v0.1.11
|
|
190
|
+
[0.1.10]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.9...v0.1.10
|
|
191
|
+
[0.1.9]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.8...v0.1.9
|
|
171
192
|
[0.1.8]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.7...v0.1.8
|
|
172
193
|
[0.1.7]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.6...v0.1.7
|
|
194
|
+
[0.1.6]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.5...v0.1.6
|
|
173
195
|
[0.1.5]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.4...v0.1.5
|
|
174
196
|
[0.1.4]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.3...v0.1.4
|
|
175
197
|
[0.1.3]: https://github.com/makshc2/agent-orchestrator-kit/compare/v0.1.2...v0.1.3
|
package/README.md
CHANGED
|
@@ -34,17 +34,18 @@ Each role runs in a **separate agent session**. The parent `/opsx:*` session is
|
|
|
34
34
|
| `spec-archiver` | Delta merge and completed-change archive |
|
|
35
35
|
| `session-handoff` | Restore/persist Memory + `handoff.md` + expanded next-thread prompt |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Delegation is **differentiated per phase** (lean model): thinking-heavy phases spawn a mandatory specialist, apply is parent-driven, archive is a deterministic CLI:
|
|
38
38
|
|
|
39
|
-
| Phase / signal |
|
|
40
|
-
|
|
39
|
+
| Phase / signal | Delegation |
|
|
40
|
+
|----------------|------------|
|
|
41
41
|
| Status / gates / next command | `openspec-guide` |
|
|
42
|
-
| Session start restore / session exit persist | `session-handoff` |
|
|
42
|
+
| Session start restore / session exit persist | parent-driven CLI (`handoff --restore` / `handoff <name>`); `session-handoff` is a fallback only |
|
|
43
43
|
| Kit / MCP / sync repair | `setup-doctor` |
|
|
44
|
-
| Explore repository research | `codebase-explorer` |
|
|
45
|
-
| Design / propose / spec review | `design-intake` / `spec-architect` / `spec-reviewer` |
|
|
46
|
-
| Apply
|
|
47
|
-
|
|
|
44
|
+
| Explore repository research | `codebase-explorer` (mandatory) |
|
|
45
|
+
| Design / propose / spec review | `design-intake` / `spec-architect` / `spec-reviewer` (mandatory) |
|
|
46
|
+
| Apply | parent implements from `tasks.md` + `apply-notes.md`; `code-writer` / `test-writer` optional for ≥ 2 independent tasks or explicit request; `design-implementer` for design-brief/Figma tasks |
|
|
47
|
+
| Pre-PR code review | `code-reviewer` |
|
|
48
|
+
| Archive | `npx agent-orchestrator-kit archive <name>` (CLI — no subagent; `spec-archiver` is a fallback when the CLI is unavailable) |
|
|
48
49
|
|
|
49
50
|
- **Cursor** → `.cursor/agents/` (native subagents)
|
|
50
51
|
- **Claude Code** → `.claude/agents/` (native subagents)
|
|
@@ -73,7 +74,7 @@ npx agent-orchestrator-kit@latest init --profile generic --ci gitlab --spec-veri
|
|
|
73
74
|
|
|
74
75
|
See [Installation](#installation) for profile/CI options.
|
|
75
76
|
|
|
76
|
-
**🔄 Already have the kit installed? Upgrade to latest (
|
|
77
|
+
**🔄 Already have the kit installed? Upgrade to latest (lean pipeline v2 / archive CLI in v0.3.0+, handoff CLI in v0.1.14+, Figma PAT in v0.1.11+):**
|
|
77
78
|
|
|
78
79
|
```bash
|
|
79
80
|
npx agent-orchestrator-kit@latest update
|
|
@@ -188,7 +189,7 @@ your-project/
|
|
|
188
189
|
| OpenSpec skills | All 7 skills for `/opsx:*` workflow |
|
|
189
190
|
| IDE sync | Cursor + Claude Code sync script (`--delete` semantics — removes stale skills/subagents) |
|
|
190
191
|
| 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 |
|
|
191
|
-
| CLI gates | `npx agent-orchestrator-kit status` / `gate-check` / `handoff` / `memory-setup` — deterministic review-gate and session-handoff (always via `npx`; see `cli-via-npm.mdc`) |
|
|
192
|
+
| CLI gates | `npx agent-orchestrator-kit status` / `gate-check` / `archive` / `handoff` / `memory-setup` — deterministic review-gate, archive, and session-handoff (always via `npx`; see `cli-via-npm.mdc`) |
|
|
192
193
|
| CI | `agent-verify.yml` — GitHub (default) or GitLab fragment + `prebuild` hook, both run `gate-check` |
|
|
193
194
|
| AI Spec Verifier | `spec-verify.yml` + verifier scripts — GitLab or GitHub, opt-in (`--spec-verify`) |
|
|
194
195
|
| MCP templates | Memory MCP for Cursor and Amp |
|
|
@@ -351,13 +352,13 @@ npx openspec validate <name> --strict --type change # must be ✓
|
|
|
351
352
|
**Model:** medium or strong.
|
|
352
353
|
**Purpose:** Review artifacts. Output **Approve ✓** or **Request Changes ✗**.
|
|
353
354
|
|
|
354
|
-
|
|
355
|
+
Review is **two-tiered**. Tier 1 is deterministic: `npx agent-orchestrator-kit gate-check --review <name>` runs strict OpenSpec validation, the task-contract lint, and structural checks (Non-goals / Acceptance criteria in `proposal.md`, non-empty delta-spec sections). If Tier 1 fails, the verdict is REQUEST CHANGES without spawning anyone. Only on a Tier 1 pass does the conductor spawn `spec-reviewer` (not `code-reviewer`) for Tier 2 judgment and verify its `review.md`. On APPROVE the reviewer also writes `apply-notes.md` (≤ 20 lines of constraints and pitfalls for the implementer).
|
|
355
356
|
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
- Tasks ≤ ~2 hours each
|
|
357
|
+
Tier 2 checks (judgment only — no duplication of Tier 1):
|
|
358
|
+
- Consistency proposal ↔ design ↔ tasks
|
|
359
359
|
- No scope creep vs Non-goals
|
|
360
360
|
- No conflicts with existing domain specs
|
|
361
|
+
- Tasks self-sufficient for a blind implementer (Files / Do / Done-when)
|
|
361
362
|
|
|
362
363
|
```
|
|
363
364
|
/opsx:review add-bulk-camera-export
|
|
@@ -475,6 +476,18 @@ npx agent-orchestrator-kit gate-check [change-name] [--src-glob src/] [--base HE
|
|
|
475
476
|
|
|
476
477
|
Fails (non-zero exit) when `pipeline.require_spec_review: true`, the diff against `--base` touches `--src-glob`, and the active change has no `review.md` with `Verdict: APPROVE`. When `pipeline.require_design_brief: true` and `src/` changed, it also requires `design-brief.md` (or a `Design: none` line in `proposal.md` for non-UI changes). It degrades gracefully to exit 0 (with a message, not silently) when: `.agents/orchestrator.yaml` is missing, neither review nor design brief is required, the diff can't be computed (e.g. shallow clone), or nothing under `--src-glob` changed. It also warns (never fails) when active changes exceed `pipeline.max_active_changes`. Both `agent-verify.yml` fragments (GitHub and GitLab) call `gate-check` automatically.
|
|
477
478
|
|
|
479
|
+
```bash
|
|
480
|
+
npx agent-orchestrator-kit gate-check --tasks <change-name>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Lints the task contract in `tasks.md`: every task needs `Files:` / `Do:` / `Done-when:`, no vague phrasing (`as needed`, `if necessary`, …), and every `Files:` path must exist unless prefixed `new file:`. Behavior follows `pipeline.task_contract` in `orchestrator.yaml`: `warn` (default) exits 0 with warnings, `strict` exits 1 on violations, `off` skips the lint.
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
npx agent-orchestrator-kit gate-check --review <change-name> [--json]
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Deterministic Tier 1 of the review phase: strict OpenSpec validation, the task-contract lint, `Non-goals` / `Acceptance criteria` sections in `proposal.md`, and non-empty ADDED/MODIFIED/REMOVED sections in delta specs. Human-readable stdout, or `--json` for a `{pass, errors[]}` report.
|
|
490
|
+
|
|
478
491
|
---
|
|
479
492
|
|
|
480
493
|
### Design intake: `/opsx:design`
|
|
@@ -525,7 +538,13 @@ After PR merged + CI green:
|
|
|
525
538
|
/opsx:archive add-bulk-camera-export
|
|
526
539
|
```
|
|
527
540
|
|
|
528
|
-
|
|
541
|
+
Archive is a **deterministic CLI**, not an agent workflow:
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
npx agent-orchestrator-kit archive <name> [--sync | --no-sync --force]
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
It checks the gates (APPROVE in `review.md` when required, all tasks `[x]`, target folder free), merges delta specs into `openspec/specs/` (`--sync`: ADDED append, MODIFIED replace, REMOVED delete), moves the change to `openspec/changes/archive/YYYY-MM-DD-<name>`, and runs `npx openspec validate --all --strict` with a full rollback on failure (main specs restored, new spec files deleted, move reverted). With delta specs present you must decide: `--sync` merges, `--no-sync --force` archives without merging, and no flag refuses with exit 1. It finishes by writing the final `handoff.md` (`next_command: none`) and updating memory. The `/opsx:archive` command is a thin wrapper that calls this CLI; the `spec-archiver` subagent remains only as a fallback when the CLI is unavailable.
|
|
529
548
|
|
|
530
549
|
## Configuration
|
|
531
550
|
|
|
@@ -674,7 +693,7 @@ Handoff:add-bulk-export next_role: implementer, next_command: /opsx:apply add
|
|
|
674
693
|
session_count: 2, summary: ..., blocked: none
|
|
675
694
|
```
|
|
676
695
|
|
|
677
|
-
|
|
696
|
+
Session boundaries are **parent-driven** (no routine subagent): every `/opsx:*` session restores via `npx agent-orchestrator-kit handoff --restore` (the CLI briefing already reads memory.json and `handoff.md`), falling back to reading `handoff.md` directly if the CLI fails. At exit the parent itself writes `handoff.md`, runs `npx agent-orchestrator-kit handoff <name>` (upserts `.cursor/memory.json` with an absolute path), and pastes the CLI stdout prompt. The `session-handoff` subagent is spawned only when the CLI path fails; Memory MCP is an optional mirror. The prompt is self-contained — Amp often skips Memory MCP, so the next thread must be able to work from the pasted text alone. Never configure Memory with a relative `MEMORY_FILE_PATH`; use `scripts/memory-mcp-launcher.cjs` (`npx agent-orchestrator-kit memory-setup`). The next phase always starts in a new chat. The canonical Session Start / Exit protocol lives in one place — `.agents/rules/session-handoff.mdc` — and the `/opsx:*` commands reference it instead of duplicating it.
|
|
678
697
|
|
|
679
698
|
## Amp Code — Deep Integration Notes
|
|
680
699
|
|
|
@@ -736,9 +755,15 @@ npx agent-orchestrator-kit status
|
|
|
736
755
|
npx agent-orchestrator-kit gate-check [change-name] [options]
|
|
737
756
|
--src-glob <glob> Source path filter used to detect code changes (default: src/)
|
|
738
757
|
--base <ref> Git ref to diff against (default: HEAD~1)
|
|
758
|
+
--tasks <name> Lint task contracts (Files / Do / Done-when)
|
|
759
|
+
--review <name> Deterministic Tier 1 review (optional --json)
|
|
739
760
|
Exit non-zero when require_spec_review is true, src/ changed, and the
|
|
740
761
|
active change has no review.md with Verdict: APPROVE. Graceful no-op
|
|
741
762
|
otherwise (missing config, review not required, no relevant diff).
|
|
763
|
+
|
|
764
|
+
npx agent-orchestrator-kit archive <name> [--sync | --no-sync --force]
|
|
765
|
+
Gate-check a completed change, optionally merge delta specs, move to
|
|
766
|
+
openspec/changes/archive/YYYY-MM-DD-<name>, validate, write final handoff
|
|
742
767
|
```
|
|
743
768
|
|
|
744
769
|
## Directory Reference
|
|
@@ -776,6 +801,15 @@ openspec/ # Committed — spec-driven workflow
|
|
|
776
801
|
|
|
777
802
|
## Changelog
|
|
778
803
|
|
|
804
|
+
### 0.3.0
|
|
805
|
+
- **`archive` CLI** — deterministic archive with gates, `--sync` delta merge, and rollback on validate failure
|
|
806
|
+
- **Task contract** — `gate-check --tasks` enforces `Files:` / `Do:` / `Done-when:` (`pipeline.task_contract: warn|strict|off`)
|
|
807
|
+
- **Tiered review** — `gate-check --review` is machine Tier 1; `spec-reviewer` writes `apply-notes.md` on APPROVE
|
|
808
|
+
- Lean apply (parent-driven) and parent-driven session handoff; `opsx-archive.md` is a thin CLI wrapper
|
|
809
|
+
|
|
810
|
+
### 0.2.0
|
|
811
|
+
- Thinned always-apply rules and `AGENTS.md` / `CLAUDE.md` (context budget); details stay in on-demand skills
|
|
812
|
+
|
|
779
813
|
### 0.1.14
|
|
780
814
|
- **HARD STOP session handoff** — `session-handoff` subagent at start/exit; Amp isolated `subagent-session-handoff`
|
|
781
815
|
- **`handoff` CLI** — writes `handoff.md`, upserts `.cursor/memory.json` with an absolute path, prints an expanded self-contained next-thread prompt
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSync, writeFileSync, rmSync } from 'fs';
|
|
4
|
+
import { readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSync, writeFileSync, rmSync, renameSync } from 'fs';
|
|
5
5
|
import { join, dirname, basename, resolve } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
@@ -747,8 +747,8 @@ function parseReviewVerdict(changeDir) {
|
|
|
747
747
|
const reviewPath = join(changeDir, 'review.md');
|
|
748
748
|
if (!existsSync(reviewPath)) return null;
|
|
749
749
|
const content = readFileSync(reviewPath, 'utf-8');
|
|
750
|
-
const match = content.match(
|
|
751
|
-
return match ? match[1].trim() : 'unknown';
|
|
750
|
+
const match = content.match(/^(?:#{1,6}\s*)?\*{0,2}Verdict:\*{0,2}\s*(.+?)\s*$/m);
|
|
751
|
+
return match ? match[1].replace(/\*+\s*$/, '').trim() : 'unknown';
|
|
752
752
|
}
|
|
753
753
|
|
|
754
754
|
function parseDesignBrief(changeDir) {
|
|
@@ -769,13 +769,237 @@ function readPipelineConfig(projectDir) {
|
|
|
769
769
|
const requireReviewMatch = content.match(/require_spec_review:\s*(true|false)/);
|
|
770
770
|
const requireBriefMatch = content.match(/require_design_brief:\s*(true|false)/);
|
|
771
771
|
const maxActiveMatch = content.match(/max_active_changes:\s*(\d+)/);
|
|
772
|
+
const taskContractMatch = content.match(/task_contract:\s*(warn|strict|off)/);
|
|
772
773
|
return {
|
|
773
774
|
requireSpecReview: requireReviewMatch ? requireReviewMatch[1] === 'true' : true,
|
|
774
775
|
requireDesignBrief: requireBriefMatch ? requireBriefMatch[1] === 'true' : false,
|
|
775
776
|
maxActiveChanges: maxActiveMatch ? parseInt(maxActiveMatch[1], 10) : null,
|
|
777
|
+
taskContract: taskContractMatch ? taskContractMatch[1] : 'warn',
|
|
776
778
|
};
|
|
777
779
|
}
|
|
778
780
|
|
|
781
|
+
// --- Task-contract lint (gate-check --tasks) ---
|
|
782
|
+
|
|
783
|
+
const VAGUE_DO_PATTERNS = [/\bas needed\b/i, /\bif necessary\b/i, /\bas appropriate\b/i, /де потрібно/i, /за потреби/i];
|
|
784
|
+
|
|
785
|
+
function taskContractMode(projectDir) {
|
|
786
|
+
const config = readPipelineConfig(projectDir);
|
|
787
|
+
return config ? config.taskContract : 'warn';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function parseTaskContracts(content) {
|
|
791
|
+
const tasks = [];
|
|
792
|
+
let current = null;
|
|
793
|
+
for (const line of content.split('\n')) {
|
|
794
|
+
const taskMatch = line.match(/^\s*- \[[ xX]\]\s+(.*)$/);
|
|
795
|
+
if (taskMatch) {
|
|
796
|
+
current = { title: taskMatch[1].trim(), files: null, do: null, doneWhen: null };
|
|
797
|
+
tasks.push(current);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (!current) continue;
|
|
801
|
+
const fieldMatch = line.match(/^\s+(Files|Do|Done-when):\s*(.*)$/);
|
|
802
|
+
if (fieldMatch) {
|
|
803
|
+
const value = fieldMatch[2].trim();
|
|
804
|
+
if (fieldMatch[1] === 'Files') current.files = value;
|
|
805
|
+
else if (fieldMatch[1] === 'Do') current.do = value;
|
|
806
|
+
else current.doneWhen = value;
|
|
807
|
+
} else if (/^\S/.test(line)) {
|
|
808
|
+
current = null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return tasks;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function lintTaskContracts(projectDir, tasksPath) {
|
|
815
|
+
const errors = [];
|
|
816
|
+
const tasks = parseTaskContracts(readFileSync(tasksPath, 'utf-8'));
|
|
817
|
+
for (const task of tasks) {
|
|
818
|
+
const label = `task "${task.title.slice(0, 60)}"`;
|
|
819
|
+
if (!task.files) errors.push(`${label}: missing Files:`);
|
|
820
|
+
if (!task.do) errors.push(`${label}: missing Do:`);
|
|
821
|
+
if (!task.doneWhen) errors.push(`${label}: missing Done-when:`);
|
|
822
|
+
if (task.do) {
|
|
823
|
+
for (const pattern of VAGUE_DO_PATTERNS) {
|
|
824
|
+
const match = task.do.match(pattern);
|
|
825
|
+
if (match) errors.push(`${label}: vague wording in Do: "${match[0]}"`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (task.files) {
|
|
829
|
+
for (const entry of task.files.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
830
|
+
if (/^new file:/i.test(entry)) continue;
|
|
831
|
+
if (!existsSync(join(projectDir, entry))) errors.push(`${label}: Files: path does not exist: ${entry} (prefix with "new file:" if intentional)`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return errors;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function runTasksLint(projectDir, name, { quiet = false } = {}) {
|
|
839
|
+
const mode = taskContractMode(projectDir);
|
|
840
|
+
const report = { mode, errors: [], warnings: [] };
|
|
841
|
+
if (mode === 'off') return report;
|
|
842
|
+
const tasksPath = join(projectDir, 'openspec', 'changes', name, 'tasks.md');
|
|
843
|
+
if (!existsSync(tasksPath)) {
|
|
844
|
+
report.warnings.push(`tasks.md not found: ${tasksPath}`);
|
|
845
|
+
return report;
|
|
846
|
+
}
|
|
847
|
+
const findings = lintTaskContracts(projectDir, tasksPath);
|
|
848
|
+
if (mode === 'strict') report.errors = findings;
|
|
849
|
+
else report.warnings = [...report.warnings, ...findings];
|
|
850
|
+
if (!quiet) {
|
|
851
|
+
for (const e of report.errors) log.err(e);
|
|
852
|
+
for (const w of report.warnings) log.warn(w);
|
|
853
|
+
}
|
|
854
|
+
return report;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// --- Tier 1 review (gate-check --review) ---
|
|
858
|
+
|
|
859
|
+
// Change names are interpolated into shell commands and paths; keep them slugs.
|
|
860
|
+
function isSafeChangeName(name) {
|
|
861
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function runTier1Review(projectDir, name) {
|
|
865
|
+
const errors = [];
|
|
866
|
+
if (!isSafeChangeName(name)) {
|
|
867
|
+
return { pass: false, errors: [`invalid change name: ${name}`] };
|
|
868
|
+
}
|
|
869
|
+
const changeDir = join(projectDir, 'openspec', 'changes', name);
|
|
870
|
+
if (!existsSync(changeDir)) {
|
|
871
|
+
return { pass: false, errors: [`change not found: ${name}`] };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
execSync(`npx openspec validate ${name} --strict --type change`, {
|
|
876
|
+
cwd: projectDir,
|
|
877
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
878
|
+
encoding: 'utf-8',
|
|
879
|
+
});
|
|
880
|
+
} catch (e) {
|
|
881
|
+
const detail = `${e.stdout || ''}${e.stderr || ''}`.trim().split('\n')[0] || 'non-zero exit';
|
|
882
|
+
errors.push(`openspec validate --strict failed: ${detail}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const lint = runTasksLint(projectDir, name, { quiet: true });
|
|
886
|
+
errors.push(...lint.errors);
|
|
887
|
+
|
|
888
|
+
const proposalPath = join(changeDir, 'proposal.md');
|
|
889
|
+
if (!existsSync(proposalPath)) {
|
|
890
|
+
errors.push('proposal.md not found');
|
|
891
|
+
} else {
|
|
892
|
+
const proposal = readFileSync(proposalPath, 'utf-8');
|
|
893
|
+
if (!/^#{2,}\s*Non-goals\b/im.test(proposal)) errors.push('proposal.md: missing "Non-goals" section');
|
|
894
|
+
if (!/^#{2,}\s*Acceptance criteria\b/im.test(proposal)) errors.push('proposal.md: missing "Acceptance criteria" section');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
for (const deltaPath of listDeltaSpecFiles(changeDir)) {
|
|
898
|
+
const rel = deltaPath.replace(`${projectDir}/`, '');
|
|
899
|
+
const sections = parseDeltaSpec(readFileSync(deltaPath, 'utf-8'));
|
|
900
|
+
const total = sections.ADDED.length + sections.MODIFIED.length + sections.REMOVED.length;
|
|
901
|
+
if (total === 0) errors.push(`${rel}: no non-empty ADDED/MODIFIED/REMOVED Requirements section`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return { pass: errors.length === 0, errors, warnings: lint.warnings };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// --- Delta spec sync (archive --sync) ---
|
|
908
|
+
|
|
909
|
+
function listDeltaSpecFiles(changeDir) {
|
|
910
|
+
const specsDir = join(changeDir, 'specs');
|
|
911
|
+
if (!existsSync(specsDir)) return [];
|
|
912
|
+
const files = [];
|
|
913
|
+
const walk = (dir) => {
|
|
914
|
+
for (const entry of readdirSync(dir)) {
|
|
915
|
+
const full = join(dir, entry);
|
|
916
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
917
|
+
else if (entry.endsWith('.md')) files.push(full);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
walk(specsDir);
|
|
921
|
+
return files.sort();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function splitRequirementBlocks(sectionBody) {
|
|
925
|
+
return String(sectionBody || '')
|
|
926
|
+
.split(/^### Requirement: /m)
|
|
927
|
+
.slice(1)
|
|
928
|
+
.map((part) => {
|
|
929
|
+
const nl = part.indexOf('\n');
|
|
930
|
+
const name = (nl === -1 ? part : part.slice(0, nl)).trim();
|
|
931
|
+
const body = nl === -1 ? '' : part.slice(nl + 1);
|
|
932
|
+
return { name, block: `### Requirement: ${name}\n${body}`.replace(/\s+$/, '') };
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function parseDeltaSpec(content) {
|
|
937
|
+
const sections = { ADDED: [], MODIFIED: [], REMOVED: [] };
|
|
938
|
+
const parts = String(content || '').split(/^## /m);
|
|
939
|
+
for (const part of parts.slice(1)) {
|
|
940
|
+
const nl = part.indexOf('\n');
|
|
941
|
+
const title = (nl === -1 ? part : part.slice(0, nl)).trim();
|
|
942
|
+
const body = nl === -1 ? '' : part.slice(nl + 1);
|
|
943
|
+
const match = title.match(/^(ADDED|MODIFIED|REMOVED) Requirements$/);
|
|
944
|
+
if (match) sections[match[1]] = splitRequirementBlocks(body);
|
|
945
|
+
}
|
|
946
|
+
return sections;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function findRequirementSpan(content, name) {
|
|
950
|
+
const header = `### Requirement: ${name}`;
|
|
951
|
+
const idx = content.indexOf(header);
|
|
952
|
+
if (idx === -1) return null;
|
|
953
|
+
const rest = content.slice(idx + header.length);
|
|
954
|
+
const relEnd = rest.search(/\n### Requirement: |\n## /);
|
|
955
|
+
const end = relEnd === -1 ? content.length : idx + header.length + relEnd;
|
|
956
|
+
return [idx, end];
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function planSpecSync(projectDir, deltaSpecPaths, changeName) {
|
|
960
|
+
const plan = [];
|
|
961
|
+
const conflicts = [];
|
|
962
|
+
for (const deltaPath of deltaSpecPaths) {
|
|
963
|
+
const capability = basename(dirname(deltaPath));
|
|
964
|
+
const mainPath = join(projectDir, 'openspec', 'specs', capability, 'spec.md');
|
|
965
|
+
const delta = parseDeltaSpec(readFileSync(deltaPath, 'utf-8'));
|
|
966
|
+
if (delta.ADDED.length + delta.MODIFIED.length + delta.REMOVED.length === 0) continue;
|
|
967
|
+
const existed = existsSync(mainPath);
|
|
968
|
+
const dirExisted = existsSync(dirname(mainPath));
|
|
969
|
+
const oldContent = existed ? readFileSync(mainPath, 'utf-8') : null;
|
|
970
|
+
let content = existed
|
|
971
|
+
? oldContent
|
|
972
|
+
: `## Purpose\n\n${capability} — requirements merged from change ${changeName}.\n\n## Requirements\n`;
|
|
973
|
+
|
|
974
|
+
for (const req of delta.REMOVED) {
|
|
975
|
+
const span = findRequirementSpan(content, req.name);
|
|
976
|
+
if (!span) {
|
|
977
|
+
conflicts.push(`${capability}: REMOVED requirement not found in main spec: "${req.name}"`);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
content = `${content.slice(0, span[0]).replace(/\n+$/, '\n\n')}${content.slice(span[1]).replace(/^\n+/, '')}`;
|
|
981
|
+
}
|
|
982
|
+
for (const req of delta.MODIFIED) {
|
|
983
|
+
const span = findRequirementSpan(content, req.name);
|
|
984
|
+
if (!span) {
|
|
985
|
+
conflicts.push(`${capability}: MODIFIED requirement not found in main spec: "${req.name}"`);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
content = `${content.slice(0, span[0])}${req.block}\n\n${content.slice(span[1]).replace(/^\n+/, '')}`;
|
|
989
|
+
}
|
|
990
|
+
for (const req of delta.ADDED) {
|
|
991
|
+
if (findRequirementSpan(content, req.name)) {
|
|
992
|
+
conflicts.push(`${capability}: ADDED requirement already exists in main spec: "${req.name}"`);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
content = `${content.replace(/\s+$/, '')}\n\n${req.block}\n`;
|
|
996
|
+
}
|
|
997
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
998
|
+
plan.push({ mainPath, existed, dirExisted, oldContent, newContent: content });
|
|
999
|
+
}
|
|
1000
|
+
return { plan, conflicts };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
779
1003
|
// Returns true/false when the diff is known, or null when it could not be
|
|
780
1004
|
// determined (no git repo, invalid base ref, shallow clone, etc.) — callers
|
|
781
1005
|
// must treat null as "skip gracefully", never as "block".
|
|
@@ -1354,8 +1578,46 @@ program
|
|
|
1354
1578
|
.description('Deterministically check the review gate before apply/merge (exit non-zero if unmet)')
|
|
1355
1579
|
.option('--src-glob <glob>', 'source path filter used to detect code changes', 'src/')
|
|
1356
1580
|
.option('--base <ref>', 'git ref to diff against', 'HEAD~1')
|
|
1581
|
+
.option('--tasks <name>', 'lint task contracts (Files/Do/Done-when) of a change')
|
|
1582
|
+
.option('--review <name>', 'run deterministic Tier 1 review checks on a change')
|
|
1583
|
+
.option('--json', 'with --review: print a {pass, errors[]} JSON report to stdout', false)
|
|
1357
1584
|
.action((changeName, opts) => {
|
|
1358
1585
|
const projectDir = process.cwd();
|
|
1586
|
+
|
|
1587
|
+
if (opts.tasks) {
|
|
1588
|
+
log.title(`gate-check --tasks ${opts.tasks}`);
|
|
1589
|
+
const mode = taskContractMode(projectDir);
|
|
1590
|
+
if (mode === 'off') {
|
|
1591
|
+
log.info('task contract lint disabled (pipeline.task_contract: off)');
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const report = runTasksLint(projectDir, opts.tasks);
|
|
1595
|
+
if (report.errors.length) {
|
|
1596
|
+
console.error(`task contract gate failed — ${report.errors.length} error(s) (pipeline.task_contract: strict)`);
|
|
1597
|
+
process.exitCode = 1;
|
|
1598
|
+
} else if (report.warnings.length) {
|
|
1599
|
+
log.warn(`task contract: ${report.warnings.length} issue(s) — warn mode, not blocking`);
|
|
1600
|
+
} else {
|
|
1601
|
+
log.ok('all tasks follow the contract (Files/Do/Done-when)');
|
|
1602
|
+
}
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if (opts.review) {
|
|
1607
|
+
const result = runTier1Review(projectDir, opts.review);
|
|
1608
|
+
if (opts.json) {
|
|
1609
|
+
console.log(JSON.stringify({ pass: result.pass, errors: result.errors }, null, 2));
|
|
1610
|
+
} else {
|
|
1611
|
+
log.title(`gate-check --review ${opts.review}`);
|
|
1612
|
+
for (const e of result.errors) log.err(e);
|
|
1613
|
+
for (const w of result.warnings || []) log.warn(w);
|
|
1614
|
+
if (result.pass) log.ok('Tier 1 review passed — proceed to spec-reviewer (Tier 2)');
|
|
1615
|
+
else log.err(`Tier 1 review failed — ${result.errors.length} error(s)`);
|
|
1616
|
+
}
|
|
1617
|
+
if (!result.pass) process.exitCode = 1;
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1359
1621
|
log.title('agent-orchestrator gate-check');
|
|
1360
1622
|
|
|
1361
1623
|
const config = readPipelineConfig(projectDir);
|
|
@@ -1427,6 +1689,146 @@ program
|
|
|
1427
1689
|
}
|
|
1428
1690
|
});
|
|
1429
1691
|
|
|
1692
|
+
program
|
|
1693
|
+
.command('archive <name>')
|
|
1694
|
+
.description('Archive a completed change: check gates, optionally sync delta specs, move to a dated archive, validate, write final handoff')
|
|
1695
|
+
.option('--sync', 'merge delta specs into openspec/specs/ before archiving')
|
|
1696
|
+
.option('--no-sync', 'skip delta-spec merge (requires --force when delta specs exist)')
|
|
1697
|
+
.option('--force', 'confirm archiving without merge when delta specs exist', false)
|
|
1698
|
+
.action((name, opts) => {
|
|
1699
|
+
const projectDir = process.cwd();
|
|
1700
|
+
const fail = (msg) => {
|
|
1701
|
+
console.error(msg);
|
|
1702
|
+
process.exitCode = 1;
|
|
1703
|
+
};
|
|
1704
|
+
log.title(`agent-orchestrator archive ${name}`);
|
|
1705
|
+
|
|
1706
|
+
if (!isSafeChangeName(name)) return fail(`invalid change name: ${name}`);
|
|
1707
|
+
|
|
1708
|
+
let status;
|
|
1709
|
+
try {
|
|
1710
|
+
const out = execSync(`npx openspec status --change ${name} --json`, {
|
|
1711
|
+
cwd: projectDir,
|
|
1712
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1713
|
+
encoding: 'utf-8',
|
|
1714
|
+
});
|
|
1715
|
+
status = JSON.parse(out);
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
const detail = `${(e.stderr || e.stdout || e.message || '')}`.trim().split('\n')[0];
|
|
1718
|
+
return fail(`could not resolve change "${name}" via openspec status: ${detail}`);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const changeRoot = status.changeRoot || join(projectDir, 'openspec', 'changes', name);
|
|
1722
|
+
const changesDir = (status.planningHome && status.planningHome.changesDir) || join(projectDir, 'openspec', 'changes');
|
|
1723
|
+
if (!existsSync(changeRoot)) return fail(`change not found: ${changeRoot}`);
|
|
1724
|
+
|
|
1725
|
+
// Gate 1: review verdict (only when required by pipeline config)
|
|
1726
|
+
const config = readPipelineConfig(projectDir);
|
|
1727
|
+
const requireReview = config ? config.requireSpecReview : true;
|
|
1728
|
+
if (requireReview) {
|
|
1729
|
+
const verdict = parseReviewVerdict(changeRoot);
|
|
1730
|
+
if (!(verdict && /^APPROVE/i.test(verdict))) {
|
|
1731
|
+
return fail(`review gate failed — change "${name}" has ${verdict ? `verdict "${verdict}"` : 'no review.md'} (require_spec_review: true)`);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Gate 2: all tasks checked (skipped when the schema has no tasks artifact)
|
|
1736
|
+
const tasksPaths = (status.artifactPaths && status.artifactPaths.tasks && status.artifactPaths.tasks.existingOutputPaths) || [];
|
|
1737
|
+
for (const tasksPath of tasksPaths) {
|
|
1738
|
+
if (/^\s*- \[ \]/m.test(readFileSync(tasksPath, 'utf-8'))) {
|
|
1739
|
+
return fail(`tasks gate failed — ${tasksPath.replace(`${projectDir}/`, '')} still has unchecked "- [ ]" items`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Gate 3: target archive must not exist
|
|
1744
|
+
const dateStamp = new Date().toISOString().slice(0, 10);
|
|
1745
|
+
const targetDir = join(changesDir, 'archive', `${dateStamp}-${name}`);
|
|
1746
|
+
const targetRel = targetDir.replace(`${projectDir}/`, '');
|
|
1747
|
+
if (existsSync(targetDir)) return fail(`archive gate failed — target already exists: ${targetRel}`);
|
|
1748
|
+
|
|
1749
|
+
// Sync decision for delta specs
|
|
1750
|
+
const deltaSpecs = listDeltaSpecFiles(changeRoot);
|
|
1751
|
+
let plan = [];
|
|
1752
|
+
let syncStatus = 'no delta specs';
|
|
1753
|
+
if (deltaSpecs.length) {
|
|
1754
|
+
if (opts.sync === undefined) {
|
|
1755
|
+
return fail(`change "${name}" has ${deltaSpecs.length} delta spec(s) — pass --sync to merge them into openspec/specs/, or --no-sync --force to archive without merging`);
|
|
1756
|
+
}
|
|
1757
|
+
if (opts.sync === false && !opts.force) {
|
|
1758
|
+
return fail('refusing --no-sync without --force — delta specs would be archived without merging into openspec/specs/');
|
|
1759
|
+
}
|
|
1760
|
+
if (opts.sync) {
|
|
1761
|
+
const result = planSpecSync(projectDir, deltaSpecs, name);
|
|
1762
|
+
if (result.conflicts.length) {
|
|
1763
|
+
for (const conflict of result.conflicts) console.error(`sync conflict: ${conflict}`);
|
|
1764
|
+
return fail('delta-spec merge refused — resolve conflicts manually with the openspec-sync-specs skill, then re-run archive');
|
|
1765
|
+
}
|
|
1766
|
+
plan = result.plan;
|
|
1767
|
+
// Snapshots of affected main specs are held in plan[].oldContent for rollback.
|
|
1768
|
+
for (const entry of plan) {
|
|
1769
|
+
mkdirSync(dirname(entry.mainPath), { recursive: true });
|
|
1770
|
+
writeFileSync(entry.mainPath, entry.newContent);
|
|
1771
|
+
}
|
|
1772
|
+
syncStatus = `synced ${plan.length} main spec file(s)`;
|
|
1773
|
+
} else {
|
|
1774
|
+
syncStatus = 'skipped (--no-sync --force)';
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Move change into the dated archive
|
|
1779
|
+
mkdirSync(dirname(targetDir), { recursive: true });
|
|
1780
|
+
renameSync(changeRoot, targetDir);
|
|
1781
|
+
|
|
1782
|
+
// Strict validation with full rollback on failure
|
|
1783
|
+
try {
|
|
1784
|
+
execSync('npx openspec validate --all --strict', {
|
|
1785
|
+
cwd: projectDir,
|
|
1786
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1787
|
+
encoding: 'utf-8',
|
|
1788
|
+
});
|
|
1789
|
+
} catch (e) {
|
|
1790
|
+
renameSync(targetDir, changeRoot);
|
|
1791
|
+
for (const entry of plan) {
|
|
1792
|
+
if (entry.existed) writeFileSync(entry.mainPath, entry.oldContent);
|
|
1793
|
+
else if (entry.dirExisted) rmSync(entry.mainPath, { force: true });
|
|
1794
|
+
else rmSync(dirname(entry.mainPath), { recursive: true, force: true });
|
|
1795
|
+
}
|
|
1796
|
+
const detail = `${e.stdout || ''}${e.stderr || ''}`.trim() || 'non-zero exit';
|
|
1797
|
+
console.error(detail);
|
|
1798
|
+
return fail('openspec validate --all --strict failed — rolled back: change restored, main specs reverted to pre-sync state');
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Final handoff: pipeline is complete, no next-session prompt
|
|
1802
|
+
const progress = parseTasksProgress(targetDir);
|
|
1803
|
+
const fields = {
|
|
1804
|
+
changeName: name,
|
|
1805
|
+
closedRole: 'Archiver',
|
|
1806
|
+
change: `- name: ${name}\n- status: archived`,
|
|
1807
|
+
done: `Change archived to ${targetRel}. Delta spec sync: ${syncStatus}. openspec validate --all --strict passed.`,
|
|
1808
|
+
decisions: 'none',
|
|
1809
|
+
blocked: 'none',
|
|
1810
|
+
nextCommand: 'none',
|
|
1811
|
+
nextRole: 'none',
|
|
1812
|
+
attach: `- \`${targetRel}/\``,
|
|
1813
|
+
spawn: 'none',
|
|
1814
|
+
constraints: 'Pipeline complete — no next session.',
|
|
1815
|
+
status: 'archived',
|
|
1816
|
+
tasks: progress ? `${progress.done}/${progress.total}` : '',
|
|
1817
|
+
review: parseReviewVerdict(targetDir) || '',
|
|
1818
|
+
summary: `archived to ${targetRel}`,
|
|
1819
|
+
};
|
|
1820
|
+
writeFileSync(join(targetDir, 'handoff.md'), `${buildHandoffMarkdown(fields).trim()}\n`);
|
|
1821
|
+
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
1822
|
+
|
|
1823
|
+
console.log(`change: ${name}`);
|
|
1824
|
+
console.log(`schema: ${status.schemaName || 'unknown'}`);
|
|
1825
|
+
console.log(`archive: ${targetRel}`);
|
|
1826
|
+
console.log(`sync: ${syncStatus}`);
|
|
1827
|
+
console.log(`handoff: ${join(targetRel, 'handoff.md')} (next_command: none)`);
|
|
1828
|
+
console.log(`memory: ${memoryPath.replace(`${projectDir}/`, '')}`);
|
|
1829
|
+
log.ok(`archived ${name}`);
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1430
1832
|
program
|
|
1431
1833
|
.command('figma-setup')
|
|
1432
1834
|
.description('Create local Figma token env file (never prints the token)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, durable session handoff CLI, and optional local Figma PAT setup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -10,6 +10,7 @@ pipeline:
|
|
|
10
10
|
require_design_brief: false
|
|
11
11
|
max_active_changes: 1
|
|
12
12
|
archive_after_merge: true
|
|
13
|
+
task_contract: warn
|
|
13
14
|
|
|
14
15
|
roles:
|
|
15
16
|
explorer:
|
|
@@ -48,7 +49,7 @@ handoff:
|
|
|
48
49
|
persist_on_exit: true
|
|
49
50
|
emit_next_session_prompt: true
|
|
50
51
|
prompt_self_contained: true
|
|
51
|
-
spawn_handoff_subagent:
|
|
52
|
+
spawn_handoff_subagent: false
|
|
52
53
|
|
|
53
54
|
memory:
|
|
54
55
|
enabled: true
|
|
@@ -12,6 +12,8 @@ rules:
|
|
|
12
12
|
- Acceptance criteria — 3–5 перевірних пунктів
|
|
13
13
|
tasks:
|
|
14
14
|
- 3–7 атомарних задач
|
|
15
|
+
- "Контракт таска: індентовані поля Files: (реальні шляхи; нові — з префіксом new file:), Do: (конкретна зміна, 1–3 рядки), Done-when: (перевірна умова або команда)"
|
|
16
|
+
- Таск самодостатній без design.md; без неконкретних формулювань («as needed», «if necessary», «за потреби»)
|
|
15
17
|
- Останній пункт: build + lint
|
|
16
18
|
specs:
|
|
17
19
|
- Опційно для MVP — пропускай якщо change тривіальний
|