@songsid/agend 2.1.4-beta.7 → 2.1.4-beta.8
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/dist/channel/mcp-server.js +6 -20
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/fleet-manager.d.ts +38 -1
- package/dist/fleet-manager.js +254 -170
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
- package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -0
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
- package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +1 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -1074,19 +1074,34 @@ export class FleetManager {
|
|
|
1074
1074
|
this.logger.info({ name }, "Instance already running, skipping");
|
|
1075
1075
|
return;
|
|
1076
1076
|
}
|
|
1077
|
+
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
1077
1078
|
if (config.general_topic) {
|
|
1078
1079
|
// antigravity (agy) does not read MCP instructions — fleet context and
|
|
1079
1080
|
// routing instructions are not injected, so it cannot act as a dispatcher.
|
|
1080
|
-
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
1081
1081
|
if (backend === "antigravity") {
|
|
1082
1082
|
this.logger.warn({ name }, "antigravity backend does not support MCP instructions — general dispatcher will not work correctly");
|
|
1083
1083
|
this.notifyInstanceTopic(name, "⚠️ antigravity backend is not supported for General instances (no MCP instructions injection). Switch to claude-code or kiro-cli.");
|
|
1084
1084
|
}
|
|
1085
|
-
this.ensureGeneralInstructions(config.working_directory,
|
|
1085
|
+
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
1086
|
+
}
|
|
1087
|
+
else if (kind === "fleet-topic") {
|
|
1088
|
+
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
1089
|
+
// deliberately excluded: their workspace and conversation lifecycle are
|
|
1090
|
+
// managed independently from fleet-topic workers.
|
|
1091
|
+
try {
|
|
1092
|
+
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
1093
|
+
this.syncRoleSkills(skillsWorkDir, backend, "worker");
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
// Skill publishing is additive. A read-only or temporarily unavailable
|
|
1097
|
+
// workspace must not turn an otherwise valid worker startup into a
|
|
1098
|
+
// fleet outage.
|
|
1099
|
+
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
1100
|
+
}
|
|
1086
1101
|
}
|
|
1087
1102
|
await this.lifecycle.start(name, config, topicMode, {
|
|
1088
1103
|
kind,
|
|
1089
|
-
backend
|
|
1104
|
+
backend,
|
|
1090
1105
|
model: this.resolveInstanceModel(name).display,
|
|
1091
1106
|
});
|
|
1092
1107
|
// Only clear a stale process status after a real start succeeded. Clearing
|
|
@@ -1661,7 +1676,7 @@ export class FleetManager {
|
|
|
1661
1676
|
const generalDir = join(getAgendHome(), name);
|
|
1662
1677
|
mkdirSync(generalDir, { recursive: true });
|
|
1663
1678
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
1664
|
-
this.ensureGeneralInstructions(generalDir, backendName);
|
|
1679
|
+
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
1665
1680
|
fleet.instances[name] = {
|
|
1666
1681
|
...DEFAULT_INSTANCE_CONFIG,
|
|
1667
1682
|
working_directory: generalDir,
|
|
@@ -5482,6 +5497,10 @@ export class FleetManager {
|
|
|
5482
5497
|
"gemini-cli": "GEMINI.md",
|
|
5483
5498
|
"opencode": "AGENTS.md",
|
|
5484
5499
|
"kiro-cli": ".kiro/steering/project.md",
|
|
5500
|
+
// Grok reads AGENTS.md project docs; agy reads .agents/agents.md — the
|
|
5501
|
+
// same files their writeConfig() appends fleet instructions to.
|
|
5502
|
+
"grok": "AGENTS.md",
|
|
5503
|
+
"antigravity": ".agents/agents.md",
|
|
5485
5504
|
"mock": "CLAUDE.md",
|
|
5486
5505
|
};
|
|
5487
5506
|
static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
|
|
@@ -5491,146 +5510,33 @@ You route tasks, manage instances, enforce policies, and synthesize results.
|
|
|
5491
5510
|
Do NOT modify project files directly — delegate file changes to the project's instance.
|
|
5492
5511
|
You CAN write code snippets, explain code, and answer technical questions directly.
|
|
5493
5512
|
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
## Task Classification
|
|
5497
|
-
|
|
5498
|
-
Classify every incoming request before acting.
|
|
5499
|
-
|
|
5500
|
-
### Handle Directly (ALL conditions must be true)
|
|
5501
|
-
|
|
5502
|
-
- No file system access needed
|
|
5503
|
-
- No external execution needed
|
|
5504
|
-
- Answerable from static knowledge
|
|
5505
|
-
- ≤ 2 reasoning steps
|
|
5506
|
-
|
|
5507
|
-
Examples: Q&A, translation, fleet status queries, explaining a concept, writing code snippets.
|
|
5508
|
-
|
|
5509
|
-
### Delegate to 1 Instance
|
|
5510
|
-
|
|
5511
|
-
- Task scoped to a single project or repo
|
|
5512
|
-
- Requires file access, code changes, or execution
|
|
5513
|
-
|
|
5514
|
-
### Coordinate Multiple Instances
|
|
5515
|
-
|
|
5516
|
-
- Task spans multiple repos or domains
|
|
5517
|
-
- Requires outputs from one instance to feed into another
|
|
5518
|
-
- Benefits from parallel execution (max 3 instances per task)
|
|
5519
|
-
|
|
5520
|
-
-----
|
|
5521
|
-
|
|
5522
|
-
## Instance Discovery (in this order)
|
|
5523
|
-
1. list_teams() → reuse existing teams first
|
|
5524
|
-
2. list_instances() → find by working_directory, description, or tags
|
|
5525
|
-
3. describe_instance() → confirm capabilities before delegating
|
|
5526
|
-
4. create_instance() → only if no suitable instance exists
|
|
5527
|
-
|
|
5528
|
-
Rules: prefer reuse over creation. Do NOT create duplicates of running instances.
|
|
5529
|
-
|
|
5530
|
-
-----
|
|
5531
|
-
|
|
5532
|
-
## Delegation Protocol
|
|
5533
|
-
|
|
5534
|
-
Every delegation via send_to_instance() MUST include:
|
|
5535
|
-
|
|
5536
|
-
1. Task scope — what exactly to do, bounded clearly
|
|
5537
|
-
2. Expected output — what to return and in what form
|
|
5538
|
-
3. Policy reminder — "Follow Development Workflow policy" (for code tasks)
|
|
5539
|
-
|
|
5540
|
-
### Loop Prevention
|
|
5541
|
-
|
|
5542
|
-
- Never re-delegate a task back to the instance that sent it to you
|
|
5543
|
-
- If a task has bounced 3 times, stop and solve locally or reduce scope
|
|
5544
|
-
|
|
5545
|
-
### Execution Strategy
|
|
5546
|
-
|
|
5547
|
-
Parallel — use only when tasks are independent with no shared state
|
|
5548
|
-
Sequential — use when one task's output feeds into the next
|
|
5549
|
-
|
|
5550
|
-
-----
|
|
5513
|
+
## Task Routing
|
|
5551
5514
|
|
|
5552
|
-
|
|
5515
|
+
- **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
|
|
5516
|
+
- **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
|
|
5517
|
+
- **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
|
|
5553
5518
|
|
|
5554
|
-
|
|
5519
|
+
Instance discovery order: list_teams() → list_instances() → describe_instance() → create_instance() only if nothing fits. Prefer reuse; never duplicate a running instance.
|
|
5555
5520
|
|
|
5556
|
-
|
|
5557
|
-
- Partial → State what succeeded, what remains, proposed next steps.
|
|
5558
|
-
- Failure → Retry up to 2 times. If still failing: try alternative instance, reduce scope, or return partial result clearly marked.
|
|
5559
|
-
- No response → Ping again after reasonable wait. If still silent: report to user with options.
|
|
5521
|
+
## Reply Contract
|
|
5560
5522
|
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
Every final response to the user should contain:
|
|
5564
|
-
|
|
5565
|
-
- Result — the actual answer or deliverable
|
|
5566
|
-
- Gaps — anything incomplete or unresolved (omit if none)
|
|
5567
|
-
|
|
5568
|
-
-----
|
|
5569
|
-
|
|
5570
|
-
## Shared Decisions
|
|
5571
|
-
|
|
5572
|
-
Use post_decision() / list_decisions() for any choice that affects more than 1 instance, changes an API contract, introduces a new dependency, or alters deployment process.
|
|
5573
|
-
|
|
5574
|
-
When instances disagree, collect both viewpoints, make a decision, and record it via post_decision.
|
|
5575
|
-
|
|
5576
|
-
-----
|
|
5523
|
+
Every final response to the user contains: the result (the actual answer or deliverable) and gaps (anything incomplete — omit if none). Summarize instance reports; omit internal coordination noise.
|
|
5577
5524
|
|
|
5578
5525
|
## After Restart
|
|
5579
5526
|
|
|
5580
|
-
|
|
5581
|
-
1. list_instances() → rebuild fleet awareness
|
|
5582
|
-
2. list_teams() → restore team structure
|
|
5583
|
-
3. list_decisions() → reload policies and conventions
|
|
5584
|
-
|
|
5585
|
-
Only then handle incoming requests.
|
|
5586
|
-
|
|
5587
|
-
-----
|
|
5588
|
-
|
|
5589
|
-
## Development Workflow Policy
|
|
5590
|
-
|
|
5591
|
-
All code changes across the fleet should follow this workflow.
|
|
5592
|
-
The coordinator enforces compliance but does not perform these steps directly.
|
|
5593
|
-
Remind instances of this policy when delegating code tasks.
|
|
5594
|
-
|
|
5595
|
-
### Workflow Stages
|
|
5596
|
-
Design Proposed → Design Approved → Implementation → Submit for Review → Under Review → Approved → Merge
|
|
5597
|
-
|
|
5598
|
-
### Policy Rules
|
|
5527
|
+
BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
|
|
5599
5528
|
|
|
5600
|
-
|
|
5601
|
-
2. Challenger pairing — every code task should have a developer + reviewer. Reviewer actively questions decisions and finds risks.
|
|
5602
|
-
3. Verify by execution — backend/CLI changes must be tested by running them. Do not trust documentation alone.
|
|
5603
|
-
4. Independent review — every merge requires code review from someone other than the author.
|
|
5604
|
-
5. Root cause first — bug fixes require confirmed root cause before proposing a fix.
|
|
5605
|
-
6. Merge conditions: tests pass, reviewer approved, branch and worktree cleaned up.
|
|
5529
|
+
## Playbooks (on-demand skills)
|
|
5606
5530
|
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
-
|
|
5610
|
-
|
|
5611
|
-
- Do NOT create new instances without coordinator approval
|
|
5612
|
-
|
|
5613
|
-
-----
|
|
5614
|
-
|
|
5615
|
-
## Team Management
|
|
5616
|
-
|
|
5617
|
-
- Always check existing teams before creating new ones
|
|
5618
|
-
- Default to ephemeral teams (created for a specific task, dissolved after completion)
|
|
5619
|
-
- Clean up ephemeral teams and instances after task completion
|
|
5620
|
-
|
|
5621
|
-
-----
|
|
5622
|
-
|
|
5623
|
-
## Instance Configuration Tips
|
|
5624
|
-
|
|
5625
|
-
When users create specialized instances, suggest these configurations:
|
|
5626
|
-
|
|
5627
|
-
- **Reviewer instances**: Add \`pre_task_command: "/chat load reviewer-base"\` to reset context before each review, preventing influence from previous conversations.
|
|
5628
|
-
- **Collab mode**: For multi-bot channels, use \`/collab\` to enable @mention-based triggering.
|
|
5629
|
-
- **Cost control**: Set per-instance \`cost_guard\` for expensive backends.
|
|
5531
|
+
Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
|
|
5532
|
+
- **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
|
|
5533
|
+
- **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
|
|
5534
|
+
Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
|
|
5630
5535
|
`;
|
|
5631
5536
|
/** Ensure the general instance has its project instructions file + knowledge */
|
|
5632
|
-
ensureGeneralInstructions(workDir, backendName) {
|
|
5537
|
+
ensureGeneralInstructions(workDir, backendName, instanceName) {
|
|
5633
5538
|
const backend = backendName ?? "claude-code";
|
|
5539
|
+
workDir = this.resolveKnowledgeWorkDir(workDir, backend, instanceName);
|
|
5634
5540
|
const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
|
|
5635
5541
|
const filePath = join(workDir, filename);
|
|
5636
5542
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
@@ -5638,60 +5544,238 @@ When users create specialized instances, suggest these configurations:
|
|
|
5638
5544
|
writeFileSync(filePath, FleetManager.GENERAL_INSTRUCTIONS, "utf-8");
|
|
5639
5545
|
this.logger.info({ filePath }, "Created general instance instructions file");
|
|
5640
5546
|
}
|
|
5641
|
-
// Sync bundled knowledge files to general's steering
|
|
5547
|
+
// Sync bundled knowledge files to general's steering and skills directories.
|
|
5642
5548
|
this.syncGeneralKnowledge(workDir, backend);
|
|
5643
5549
|
}
|
|
5644
|
-
/**
|
|
5550
|
+
/** Resolve the workspace path a backend actually uses before publishing knowledge. */
|
|
5551
|
+
resolveKnowledgeWorkDir(workDir, backend, instanceName) {
|
|
5552
|
+
// Some backends relocate their real cwd (agy moves hidden paths to
|
|
5553
|
+
// ~/agend-workspaces/<name>). Instructions and skills must land where the
|
|
5554
|
+
// CLI actually runs, not where fleet.yaml points.
|
|
5555
|
+
try {
|
|
5556
|
+
const resolved = createBackend(backend, join(getAgendHome(), "cli-env"))
|
|
5557
|
+
.resolveWorkingDirectory?.(workDir, instanceName);
|
|
5558
|
+
if (resolved)
|
|
5559
|
+
workDir = resolved;
|
|
5560
|
+
}
|
|
5561
|
+
catch { /* unknown backend name — keep the raw path */ }
|
|
5562
|
+
return workDir;
|
|
5563
|
+
}
|
|
5564
|
+
/**
|
|
5565
|
+
* Where each backend natively loads on-demand skills from, relative to the
|
|
5566
|
+
* workspace. Backends without a native skill mechanism (opencode, grok,
|
|
5567
|
+
* antigravity, gemini-cli) are deliberately absent: dropping files a CLI
|
|
5568
|
+
* never reads is clutter, not capability. Unknown directories are ignored
|
|
5569
|
+
* by older CLI versions, so publishing is fail-open across upgrades.
|
|
5570
|
+
*/
|
|
5571
|
+
static SKILLS_DIR_SEGMENTS = {
|
|
5572
|
+
"kiro-cli": [".kiro", "skills"],
|
|
5573
|
+
"claude-code": [".claude", "skills"],
|
|
5574
|
+
"codex": [".agents", "skills"],
|
|
5575
|
+
// Live-verified: OpenCode and Antigravity scan .agents/skills; Grok's
|
|
5576
|
+
// vendor-canonical location is .grok/skills.
|
|
5577
|
+
"opencode": [".agents", "skills"],
|
|
5578
|
+
"grok": [".grok", "skills"],
|
|
5579
|
+
"antigravity": [".agents", "skills"],
|
|
5580
|
+
};
|
|
5581
|
+
/** Copy general-knowledge steering + all role-eligible skills to General. */
|
|
5645
5582
|
syncGeneralKnowledge(workDir, backend) {
|
|
5646
5583
|
const knowledgeDir = join(dirname(fileURLToPath(import.meta.url)), "general-knowledge");
|
|
5647
5584
|
if (!existsSync(knowledgeDir))
|
|
5648
5585
|
return;
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5586
|
+
this.syncGeneralSteering(workDir, backend, join(knowledgeDir, "steering"));
|
|
5587
|
+
this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
|
|
5588
|
+
this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
|
|
5589
|
+
}
|
|
5590
|
+
/** Publish only the bundled skills eligible for an instance role. */
|
|
5591
|
+
syncRoleSkills(workDir, backend, role, knowledgeDir) {
|
|
5592
|
+
const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
|
|
5593
|
+
if (!skillSegments)
|
|
5594
|
+
return;
|
|
5595
|
+
const root = knowledgeDir ?? join(dirname(fileURLToPath(import.meta.url)), "general-knowledge");
|
|
5596
|
+
if (!existsSync(root))
|
|
5597
|
+
return;
|
|
5598
|
+
// Before managed-skill manifests existed, only Kiro General received
|
|
5599
|
+
// bundled skills. Allow that one legacy layout to be adopted so upgrades
|
|
5600
|
+
// can keep those copies current; other backends never had unmanaged
|
|
5601
|
+
// AgEnD-published skills and must retain the normal user-ownership guard.
|
|
5602
|
+
const adoptLegacyUnmanaged = backend === "kiro-cli" && role === "general";
|
|
5603
|
+
this.syncManagedSkills(join(workDir, ...skillSegments), join(root, "skills"), role, adoptLegacyUnmanaged);
|
|
5604
|
+
this.logger.debug({ workDir, backend, role }, "Synced role-based bundled skills");
|
|
5605
|
+
}
|
|
5606
|
+
/**
|
|
5607
|
+
* Steering (always-on rules like core-rules.md). Kiro loads a native
|
|
5608
|
+
* steering directory; every other backend gets the content embedded into
|
|
5609
|
+
* its instructions file (CLAUDE.md / AGENTS.md / …) inside a managed marker
|
|
5610
|
+
* block — the previous behavior dropped bare .md files in the workspace
|
|
5611
|
+
* root, which no CLI ever read. The block is replaced in place on every
|
|
5612
|
+
* sync, so rule updates reach EXISTING workspaces; everything the user
|
|
5613
|
+
* wrote outside the markers is preserved byte-for-byte.
|
|
5614
|
+
*/
|
|
5615
|
+
syncGeneralSteering(workDir, backend, srcSteering) {
|
|
5616
|
+
if (!existsSync(srcSteering))
|
|
5617
|
+
return;
|
|
5618
|
+
const files = readdirSync(srcSteering).filter(f => f.endsWith(".md")).sort();
|
|
5619
|
+
if (files.length === 0)
|
|
5620
|
+
return;
|
|
5621
|
+
if (backend === "kiro-cli") {
|
|
5622
|
+
const steeringDir = join(workDir, ".kiro", "steering");
|
|
5623
|
+
mkdirSync(steeringDir, { recursive: true });
|
|
5624
|
+
for (const file of files) {
|
|
5660
5625
|
const dest = join(steeringDir, file);
|
|
5661
|
-
const newContent = readFileSync(
|
|
5626
|
+
const newContent = readFileSync(join(srcSteering, file), "utf-8");
|
|
5662
5627
|
try {
|
|
5663
5628
|
if (existsSync(dest) && readFileSync(dest, "utf-8") === newContent)
|
|
5664
5629
|
continue;
|
|
5665
5630
|
}
|
|
5666
|
-
catch { }
|
|
5631
|
+
catch { /* rewrite */ }
|
|
5667
5632
|
writeFileSync(dest, newContent);
|
|
5668
5633
|
}
|
|
5634
|
+
return;
|
|
5669
5635
|
}
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5636
|
+
const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
|
|
5637
|
+
const instructionsPath = join(workDir, filename);
|
|
5638
|
+
const body = files.map(f => readFileSync(join(srcSteering, f), "utf-8").trim()).join("\n\n");
|
|
5639
|
+
const block = `${FleetManager.STEERING_BLOCK_BEGIN}\n${body}\n${FleetManager.STEERING_BLOCK_END}`;
|
|
5640
|
+
let existing = "";
|
|
5641
|
+
try {
|
|
5642
|
+
existing = existsSync(instructionsPath) ? readFileSync(instructionsPath, "utf-8") : "";
|
|
5643
|
+
}
|
|
5644
|
+
catch { /* treat as empty */ }
|
|
5645
|
+
const beginAt = existing.indexOf(FleetManager.STEERING_BLOCK_BEGIN);
|
|
5646
|
+
const endAt = existing.indexOf(FleetManager.STEERING_BLOCK_END);
|
|
5647
|
+
let next;
|
|
5648
|
+
if (beginAt !== -1 && endAt !== -1 && endAt > beginAt) {
|
|
5649
|
+
next = existing.slice(0, beginAt) + block + existing.slice(endAt + FleetManager.STEERING_BLOCK_END.length);
|
|
5650
|
+
}
|
|
5651
|
+
else {
|
|
5652
|
+
next = existing.trimEnd() + (existing.trim() ? "\n\n" : "") + block + "\n";
|
|
5653
|
+
}
|
|
5654
|
+
if (next !== existing) {
|
|
5655
|
+
mkdirSync(dirname(instructionsPath), { recursive: true });
|
|
5656
|
+
writeFileSync(instructionsPath, next);
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
static STEERING_BLOCK_BEGIN = "<!-- >>> agend:core-rules — managed by AgEnD; edits inside this block are overwritten -->";
|
|
5660
|
+
static STEERING_BLOCK_END = "<!-- <<< agend:core-rules -->";
|
|
5661
|
+
/**
|
|
5662
|
+
* Publish AgEnD's bundled skills into a CLI's native skills directory,
|
|
5663
|
+
* owning ONLY what we published. A manifest records which skill names AgEnD
|
|
5664
|
+
* wrote; a bundled rename/removal deletes the stale managed copy, while a
|
|
5665
|
+
* skill the user created by hand is never listed and therefore never
|
|
5666
|
+
* touched — even if a future bundle happens to reuse its name (the sync
|
|
5667
|
+
* then skips it and logs, rather than overwrite the user's work). The sole
|
|
5668
|
+
* migration exception is the pre-manifest Kiro General layout explicitly
|
|
5669
|
+
* selected by adoptLegacyUnmanaged.
|
|
5670
|
+
*/
|
|
5671
|
+
syncManagedSkills(destSkills, srcSkills, role, adoptLegacyUnmanaged = false) {
|
|
5672
|
+
if (!existsSync(srcSkills))
|
|
5673
|
+
return;
|
|
5674
|
+
mkdirSync(destSkills, { recursive: true });
|
|
5675
|
+
const manifestPath = join(destSkills, ".agend-managed-skills.json");
|
|
5676
|
+
const hadManifest = existsSync(manifestPath);
|
|
5677
|
+
let previouslyManaged = [];
|
|
5678
|
+
try {
|
|
5679
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
5680
|
+
if (Array.isArray(parsed))
|
|
5681
|
+
previouslyManaged = parsed.filter(n => typeof n === "string");
|
|
5682
|
+
}
|
|
5683
|
+
catch { /* first sync, or corrupt manifest — treat as owning nothing */ }
|
|
5684
|
+
const bundled = readdirSync(srcSkills)
|
|
5685
|
+
.filter(name => existsSync(join(srcSkills, name, "SKILL.md")))
|
|
5686
|
+
.sort();
|
|
5687
|
+
// Pre-manifest Kiro General copied bundled skills directly into
|
|
5688
|
+
// .kiro/skills. If every existing skill is still a bundled name, this is
|
|
5689
|
+
// the unambiguous legacy layout: adopt it once, update it below, and write
|
|
5690
|
+
// the ownership manifest. Any extra skill name keeps the directory fully
|
|
5691
|
+
// on the user-owned/collision path.
|
|
5692
|
+
if (!hadManifest && adoptLegacyUnmanaged) {
|
|
5693
|
+
const existing = readdirSync(destSkills, { withFileTypes: true })
|
|
5694
|
+
.filter(entry => entry.isDirectory() && existsSync(join(destSkills, entry.name, "SKILL.md")))
|
|
5695
|
+
.map(entry => entry.name)
|
|
5696
|
+
.sort();
|
|
5697
|
+
if (existing.length > 0 && existing.every(name => bundled.includes(name))) {
|
|
5698
|
+
previouslyManaged = existing;
|
|
5699
|
+
this.logger.info({ skills: existing, destSkills }, "Adopted legacy AgEnD skills into managed ownership");
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
const eligible = bundled.filter(name => {
|
|
5703
|
+
const roles = this.readManagedSkillRoles(join(srcSkills, name, "SKILL.md"));
|
|
5704
|
+
// General is the coordinator and receives both coordinator and worker
|
|
5705
|
+
// playbooks. Workers receive only skills explicitly marked for workers.
|
|
5706
|
+
return role === "general" || roles.includes("worker");
|
|
5707
|
+
});
|
|
5708
|
+
const managed = [];
|
|
5709
|
+
for (const name of eligible) {
|
|
5710
|
+
const destDir = join(destSkills, name);
|
|
5711
|
+
const dest = join(destDir, "SKILL.md");
|
|
5712
|
+
const isOurs = previouslyManaged.includes(name) || !existsSync(destDir);
|
|
5713
|
+
if (!isOurs) {
|
|
5714
|
+
// Name collision with a user-authored skill: theirs wins, loudly.
|
|
5715
|
+
this.logger.warn({ skill: name, destSkills }, "Skipping bundled skill — a skill of this name exists but was not published by AgEnD");
|
|
5716
|
+
continue;
|
|
5692
5717
|
}
|
|
5718
|
+
managed.push(name);
|
|
5719
|
+
const newContent = readFileSync(join(srcSkills, name, "SKILL.md"), "utf-8");
|
|
5720
|
+
try {
|
|
5721
|
+
if (existsSync(dest) && readFileSync(dest, "utf-8") === newContent)
|
|
5722
|
+
continue;
|
|
5723
|
+
}
|
|
5724
|
+
catch { /* rewrite */ }
|
|
5725
|
+
mkdirSync(destDir, { recursive: true });
|
|
5726
|
+
writeFileSync(dest, newContent);
|
|
5727
|
+
}
|
|
5728
|
+
// Remove managed skills that are no longer bundled OR no longer eligible
|
|
5729
|
+
// for this role. This makes a shared → general-only metadata change take
|
|
5730
|
+
// effect on the next worker startup instead of leaving stale capability.
|
|
5731
|
+
for (const stale of previouslyManaged) {
|
|
5732
|
+
if (eligible.includes(stale))
|
|
5733
|
+
continue;
|
|
5734
|
+
try {
|
|
5735
|
+
rmSync(join(destSkills, stale), { recursive: true, force: true });
|
|
5736
|
+
this.logger.info({ skill: stale, destSkills }, "Removed retired AgEnD-managed skill");
|
|
5737
|
+
}
|
|
5738
|
+
catch (err) {
|
|
5739
|
+
this.logger.debug({ err, skill: stale }, "Failed to remove retired managed skill");
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
try {
|
|
5743
|
+
writeFileSync(manifestPath, JSON.stringify(managed, null, 2) + "\n");
|
|
5744
|
+
}
|
|
5745
|
+
catch (err) {
|
|
5746
|
+
this.logger.debug({ err, manifestPath }, "Failed to write managed-skills manifest");
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
/** Read AgEnD's roles extension from SKILL.md YAML frontmatter. */
|
|
5750
|
+
readManagedSkillRoles(skillPath) {
|
|
5751
|
+
const fallback = ["general"];
|
|
5752
|
+
try {
|
|
5753
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
5754
|
+
const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
5755
|
+
if (!match)
|
|
5756
|
+
return fallback;
|
|
5757
|
+
const document = parseDocument(match[1]);
|
|
5758
|
+
if (document.errors.length > 0)
|
|
5759
|
+
throw document.errors[0];
|
|
5760
|
+
const frontmatter = document.toJS();
|
|
5761
|
+
if (!frontmatter || frontmatter.roles === undefined)
|
|
5762
|
+
return fallback;
|
|
5763
|
+
if (!Array.isArray(frontmatter.roles))
|
|
5764
|
+
throw new Error("roles must be an array");
|
|
5765
|
+
const roles = [...new Set(frontmatter.roles)]
|
|
5766
|
+
.filter((value) => value === "general" || value === "worker");
|
|
5767
|
+
if (roles.length !== frontmatter.roles.length || roles.length === 0) {
|
|
5768
|
+
throw new Error("roles must contain only general and/or worker");
|
|
5769
|
+
}
|
|
5770
|
+
return roles;
|
|
5771
|
+
}
|
|
5772
|
+
catch (err) {
|
|
5773
|
+
// Fail closed for workers: malformed or unknown metadata keeps the
|
|
5774
|
+
// backwards-compatible General-only behavior instead of leaking an
|
|
5775
|
+
// administrative skill into worker workspaces.
|
|
5776
|
+
this.logger.warn({ err, skillPath }, "Invalid bundled skill roles — defaulting to General only");
|
|
5777
|
+
return fallback;
|
|
5693
5778
|
}
|
|
5694
|
-
this.logger.debug({ knowledgeDir, steeringDir }, "Synced general knowledge files");
|
|
5695
5779
|
}
|
|
5696
5780
|
/** Fetch forum topic icon stickers and pick emoji IDs for each state */
|
|
5697
5781
|
async resolveTopicIcons() {
|