feat: initialize VS Code Operator extension

This commit is contained in:
L'électron rare
2026-03-21 09:07:11 +01:00
commit f92e55925f
14 changed files with 1326 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
out/
.DS_Store
.vscode-test/
*.vsix
+18
View File
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Kill LIFE Operator",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: compile"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"problemMatcher": "$tsc",
"group": {
"kind": "build",
"isDefault": true
},
"label": "npm: compile"
},
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "npm: watch"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
node_modules/**
.vscode/**
src/**
tsconfig.json
README.md
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 electron-rare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+74
View File
@@ -0,0 +1,74 @@
# Kill LIFE Operator for VS Code
Extension VS Code orientee operationnel pour relier plusieurs projets locaux et piloter les agents comme un cockpit d'execution, de runbooks et de prochaines actions.
## Positionnement
`Kill LIFE Operator` est la version operationnelle du trio :
- `Studio` pour la construction produit.
- `Mesh` pour la coordination multi-repo.
- `Operator` pour l'execution, la supervision, les checks et les actions terrain.
## Ce que fait cette extension
- Scanne chaque projet ouvert dans le workspace, plus les racines ajoutees manuellement.
- Detecte par defaut `agents/*.md` et `.github/agents/*.md`.
- Affiche les agents trouves dans une sidebar dediee.
- Permet d'ouvrir un prompt agent, de le copier, ou de l'inserer dans l'editeur courant.
- Expose un participant de chat `@killops` avec des commandes `/agents`, `/pm`, `/architect`, `/firmware`, `/hw`, `/qa`, `/doc`.
Par defaut, `@killops` repond avec un biais operationnel : etat courant, checks, incidents, runbooks, priorite d'action et etapes executables.
## Demarrage rapide
```bash
npm install
npm run compile
```
Ensuite dans VS Code :
1. Ouvre ce repo.
2. Lance `F5` pour ouvrir une fenetre d'extension development host.
3. Dans la nouvelle fenetre, ouvre ton vrai workspace projet.
4. Si besoin, ajoute d'autres depots via la commande `Kill LIFE Operator: Add Linked Project Root`.
## Utilisation
### Sidebar
La vue `Kill LIFE Operator` liste les projets detectes puis les agents trouves.
- Clic sur un agent : ouvre son fichier Markdown.
- Menu contextuel : copie ou insere le prompt.
### Chat
Dans le chat Copilot / agent de VS Code :
- `@killops /agents` : liste les projets et agents lies.
- `@killops /firmware` : aide a piloter l'execution cote firmware.
- `@killops /hw` : aide a cadrer les impacts hardware et les actions.
- `@killops /qa` : aide a definir checks, validations et evidence packs.
- `@killops /doc` : aide a produire runbooks, notes et passages de relais.
- `@killops` sans commande : agit comme cockpit operationnel.
## Reglages
### `killLifeOperator.linkedProjectRoots`
Liste de chemins absolus supplementaires a scanner, en plus des dossiers deja ouverts dans le workspace.
### `killLifeOperator.agentSearchPatterns`
Globs utilises pour decouvrir les prompts d'agents dans chaque racine projet.
Valeurs par defaut :
```json
[
"agents/*.md",
".github/agents/*.md"
]
```
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
<rect width="128" height="128" rx="24" fill="#0E1A24"/>
<rect x="18" y="18" width="92" height="92" rx="18" fill="#152A3A" stroke="#4FD1C5" stroke-width="4"/>
<path d="M38 91V37H48V82H86V91H38Z" fill="#F7FAFC"/>
<path d="M60 91V37H70V56L84 42H97L80 60L99 91H87L73 68L70 71V91H60Z" fill="#4FD1C5"/>
<circle cx="93" cy="35" r="9" fill="#F6AD55"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

+287
View File
@@ -0,0 +1,287 @@
{
"name": "kill-life-operator",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kill-life-operator",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"fast-glob": "^3.3.3"
},
"devDependencies": {
"@types/node": "^25.5.0",
"@types/vscode": "^1.110.0",
"typescript": "^5.9.3"
},
"engines": {
"vscode": "^1.110.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@types/node": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/vscode": {
"version": "1.110.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz",
"integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==",
"dev": true,
"license": "MIT"
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
}
}
}
+184
View File
@@ -0,0 +1,184 @@
{
"name": "kill-life-operator",
"displayName": "Kill LIFE Operator",
"description": "Operational VS Code extension for running linked local project agents as an execution cockpit for status, runbooks, and next actions.",
"version": "0.1.0",
"publisher": "electron-rare",
"license": "MIT",
"engines": {
"vscode": "^1.110.0"
},
"categories": [
"AI",
"Other"
],
"keywords": [
"agents",
"copilot",
"chat",
"embedded",
"kill-life"
],
"activationEvents": [
"onChatParticipant:kill-life-operator.orchestrator"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "killLifeOperator.refreshAgents",
"title": "Kill LIFE Operator: Refresh Agents"
},
{
"command": "killLifeOperator.addLinkedProjectRoot",
"title": "Kill LIFE Operator: Add Linked Project Root"
},
{
"command": "killLifeOperator.removeLinkedProjectRoot",
"title": "Kill LIFE Operator: Remove Linked Project Root"
},
{
"command": "killLifeOperator.openAgentFile",
"title": "Kill LIFE Operator: Open Agent File"
},
{
"command": "killLifeOperator.copyAgentPrompt",
"title": "Kill LIFE Operator: Copy Agent Prompt"
},
{
"command": "killLifeOperator.insertAgentPrompt",
"title": "Kill LIFE Operator: Insert Agent Prompt"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "killLifeOperatorSidebar",
"title": "Kill LIFE Operator",
"icon": "media/kill-life.svg"
}
]
},
"views": {
"killLifeOperatorSidebar": [
{
"id": "killLifeOperator.agentsView",
"name": "Linked Agents"
}
]
},
"menus": {
"view/title": [
{
"command": "killLifeOperator.refreshAgents",
"when": "view == killLifeOperator.agentsView",
"group": "navigation@1"
},
{
"command": "killLifeOperator.addLinkedProjectRoot",
"when": "view == killLifeOperator.agentsView",
"group": "navigation@2"
},
{
"command": "killLifeOperator.removeLinkedProjectRoot",
"when": "view == killLifeOperator.agentsView",
"group": "navigation@3"
}
],
"view/item/context": [
{
"command": "killLifeOperator.openAgentFile",
"when": "view == killLifeOperator.agentsView && viewItem == agentItem",
"group": "inline"
},
{
"command": "killLifeOperator.copyAgentPrompt",
"when": "view == killLifeOperator.agentsView && viewItem == agentItem",
"group": "navigation@1"
},
{
"command": "killLifeOperator.insertAgentPrompt",
"when": "view == killLifeOperator.agentsView && viewItem == agentItem",
"group": "navigation@2"
}
]
},
"configuration": {
"title": "Kill LIFE Operator",
"properties": {
"killLifeOperator.linkedProjectRoots": {
"type": "array",
"default": [],
"items": {
"type": "string"
},
"description": "Additional absolute project roots to scan for agent markdown files."
},
"killLifeOperator.agentSearchPatterns": {
"type": "array",
"default": [
"agents/*.md",
".github/agents/*.md"
],
"items": {
"type": "string"
},
"description": "Glob patterns resolved relative to each project root."
}
}
},
"chatParticipants": [
{
"id": "kill-life-operator.orchestrator",
"name": "killops",
"fullName": "Kill LIFE Operator",
"description": "Drive operational next steps from your linked local agents.",
"isSticky": true,
"commands": [
{
"name": "agents",
"description": "List linked projects and discovered agents."
},
{
"name": "pm",
"description": "Use the project management agent prompt."
},
{
"name": "architect",
"description": "Use the architecture agent prompt."
},
{
"name": "firmware",
"description": "Use the firmware agent prompt."
},
{
"name": "hw",
"description": "Use the hardware schematic agent prompt."
},
{
"name": "qa",
"description": "Use the QA agent prompt."
},
{
"name": "doc",
"description": "Use the documentation agent prompt."
}
]
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"check": "tsc --noEmit -p ./"
},
"dependencies": {
"fast-glob": "^3.3.3"
},
"devDependencies": {
"@types/node": "^25.5.0",
"@types/vscode": "^1.110.0",
"typescript": "^5.9.3"
}
}
+232
View File
@@ -0,0 +1,232 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import fg from 'fast-glob';
import * as vscode from 'vscode';
import { getAllProjectRoots, getSearchPatterns } from './config';
const SEARCH_IGNORES = [
'**/.git/**',
'**/node_modules/**',
'**/out/**',
'**/dist/**',
'**/coverage/**',
'**/target/**'
];
const ROLE_LABELS: Record<string, string> = {
architect: 'Architect Agent',
doc: 'Documentation Agent',
firmware: 'Firmware Agent',
hw: 'Hardware Agent',
pm: 'PM Agent',
qa: 'QA Agent'
};
export interface AgentDefinition {
id: string;
role: string;
title: string;
description: string;
body: string;
file: vscode.Uri;
projectName: string;
projectRoot: string;
relativePath: string;
}
export interface ProjectSummary {
name: string;
root: string;
agents: AgentDefinition[];
}
function inferRole(relativePath: string): string {
const base = path.basename(relativePath, '.md').toLowerCase().replace(/[_-]?agent$/, '');
if (base === 'hw_schematic' || base === 'hw-schematic' || base === 'hwschematic') {
return 'hw';
}
return base;
}
function toDisplayRole(role: string): string {
return ROLE_LABELS[role] ?? `${role.replace(/[_-]+/g, ' ')} agent`;
}
function extractTitle(body: string, role: string): string {
for (const line of body.split(/\r?\n/u)) {
const trimmed = line.trim();
if (trimmed.startsWith('# ')) {
return trimmed.slice(2).trim();
}
}
return toDisplayRole(role);
}
function extractDescription(body: string, role: string): string {
for (const line of body.split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('-')) {
continue;
}
return trimmed;
}
return `Prompt source for ${toDisplayRole(role)}.`;
}
function findOwningProjectRoot(filePath: string, projectRoots: string[]): string | undefined {
return [...projectRoots]
.sort((left, right) => right.length - left.length)
.find(root => filePath === root || filePath.startsWith(`${root}${path.sep}`));
}
function compareAgents(left: AgentDefinition, right: AgentDefinition): number {
return (
left.projectName.localeCompare(right.projectName) ||
left.role.localeCompare(right.role) ||
left.relativePath.localeCompare(right.relativePath)
);
}
export function groupAgentsByProject(agents: AgentDefinition[]): ProjectSummary[] {
const grouped = new Map<string, ProjectSummary>();
for (const agent of agents) {
const existing = grouped.get(agent.projectRoot);
if (existing) {
existing.agents.push(agent);
continue;
}
grouped.set(agent.projectRoot, {
name: agent.projectName,
root: agent.projectRoot,
agents: [agent]
});
}
return [...grouped.values()]
.map(project => ({
...project,
agents: [...project.agents].sort(compareAgents)
}))
.sort((left, right) => left.name.localeCompare(right.name));
}
export function resolveProjectRootForActiveEditor(projectRoots: string[]): string | undefined {
const activeDocument = vscode.window.activeTextEditor?.document.uri;
if (!activeDocument || activeDocument.scheme !== 'file') {
return projectRoots[0];
}
return [...projectRoots]
.sort((left, right) => right.length - left.length)
.find(root => activeDocument.fsPath === root || activeDocument.fsPath.startsWith(`${root}${path.sep}`))
?? projectRoots[0];
}
export function selectAgentForRole(
agents: AgentDefinition[],
role: string,
preferredProjectRoot?: string
): AgentDefinition | undefined {
const candidates = agents.filter(agent => agent.role === role);
if (!candidates.length) {
return undefined;
}
return candidates.find(agent => agent.projectRoot === preferredProjectRoot) ?? candidates[0];
}
export class AgentCatalog {
private readonly onDidChangeEmitter = new vscode.EventEmitter<void>();
private agents: AgentDefinition[] = [];
private loaded = false;
private lastError: string | undefined;
readonly onDidChange = this.onDidChangeEmitter.event;
getAgents(): AgentDefinition[] {
return [...this.agents];
}
getProjects(): ProjectSummary[] {
return groupAgentsByProject(this.agents);
}
getLastError(): string | undefined {
return this.lastError;
}
async ensureLoaded(): Promise<void> {
if (!this.loaded) {
await this.refresh();
}
}
async refresh(): Promise<void> {
try {
const projectRoots = getAllProjectRoots();
const patterns = getSearchPatterns();
const discoveredFiles = new Set<string>();
for (const projectRoot of projectRoots) {
const matches = await fg(patterns, {
cwd: projectRoot,
absolute: true,
onlyFiles: true,
unique: true,
followSymbolicLinks: false,
ignore: SEARCH_IGNORES
});
for (const match of matches) {
discoveredFiles.add(path.resolve(match));
}
}
const nextAgents: AgentDefinition[] = [];
const orderedFiles = [...discoveredFiles].sort((left, right) => left.localeCompare(right));
for (const filePath of orderedFiles) {
const projectRoot = findOwningProjectRoot(filePath, projectRoots);
if (!projectRoot) {
continue;
}
const body = await fs.readFile(filePath, 'utf8');
const relativePath = path.relative(projectRoot, filePath);
const role = inferRole(relativePath);
nextAgents.push({
id: `${projectRoot}:${relativePath}`,
role,
title: extractTitle(body, role),
description: extractDescription(body, role),
body,
file: vscode.Uri.file(filePath),
projectName: path.basename(projectRoot),
projectRoot,
relativePath
});
}
this.agents = nextAgents.sort(compareAgents);
this.lastError = undefined;
this.loaded = true;
} catch (error) {
this.agents = [];
this.lastError = error instanceof Error ? error.message : String(error);
this.loaded = true;
} finally {
this.onDidChangeEmitter.fire();
}
}
dispose(): void {
this.onDidChangeEmitter.dispose();
}
}
+83
View File
@@ -0,0 +1,83 @@
import * as path from 'node:path';
import * as vscode from 'vscode';
const NAMESPACE = 'killLifeOperator';
const LINKED_PROJECT_ROOTS_KEY = 'linkedProjectRoots';
const SEARCH_PATTERNS_KEY = 'agentSearchPatterns';
const DEFAULT_SEARCH_PATTERNS = ['agents/*.md', '.github/agents/*.md'];
function getConfiguration(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(NAMESPACE);
}
function normalizeProjectRoot(input: string): string {
return path.resolve(input.trim());
}
function getConfigurationTarget(): vscode.ConfigurationTarget {
return vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.length
? vscode.ConfigurationTarget.Workspace
: vscode.ConfigurationTarget.Global;
}
export function getLinkedProjectRoots(): string[] {
const configured = getConfiguration().get<string[]>(LINKED_PROJECT_ROOTS_KEY) ?? [];
return [...new Set(configured.map(normalizeProjectRoot).filter(Boolean))];
}
export function getSearchPatterns(): string[] {
const configured = getConfiguration().get<string[]>(SEARCH_PATTERNS_KEY) ?? [];
const cleaned = configured.map(pattern => pattern.trim()).filter(Boolean);
return cleaned.length ? cleaned : DEFAULT_SEARCH_PATTERNS;
}
export function getAllProjectRoots(): string[] {
const workspaceRoots = (vscode.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath);
return [...new Set([...workspaceRoots, ...getLinkedProjectRoots()].map(normalizeProjectRoot))];
}
export async function addLinkedProjectRoot(): Promise<void> {
const selection = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: 'Link project root'
});
const selected = selection?.[0];
if (!selected) {
return;
}
const nextRoot = normalizeProjectRoot(selected.fsPath);
const current = getLinkedProjectRoots();
if (current.includes(nextRoot)) {
void vscode.window.showInformationMessage(`Project root already linked: ${nextRoot}`);
return;
}
const updated = [...current, nextRoot].sort((left, right) => left.localeCompare(right));
await getConfiguration().update(LINKED_PROJECT_ROOTS_KEY, updated, getConfigurationTarget());
void vscode.window.showInformationMessage(`Linked project root added: ${nextRoot}`);
}
export async function removeLinkedProjectRoot(): Promise<void> {
const current = getLinkedProjectRoots();
if (!current.length) {
void vscode.window.showInformationMessage('No linked project roots configured.');
return;
}
const picked = await vscode.window.showQuickPick(current, {
placeHolder: 'Select a linked project root to remove'
});
if (!picked) {
return;
}
const updated = current.filter(root => root !== picked);
await getConfiguration().update(LINKED_PROJECT_ROOTS_KEY, updated, getConfigurationTarget());
void vscode.window.showInformationMessage(`Linked project root removed: ${picked}`);
}
+265
View File
@@ -0,0 +1,265 @@
import * as vscode from 'vscode';
import {
AgentCatalog,
AgentDefinition,
resolveProjectRootForActiveEditor,
selectAgentForRole
} from './agentCatalog';
import { addLinkedProjectRoot, getAllProjectRoots, removeLinkedProjectRoot } from './config';
import { AgentsTreeProvider } from './tree';
const PARTICIPANT_ID = 'kill-life-operator.orchestrator';
const ROLE_COMMANDS = new Set(['pm', 'architect', 'firmware', 'hw', 'qa', 'doc']);
function buildRosterMarkdown(agents: AgentDefinition[]): string {
if (!agents.length) {
return 'No agents discovered yet. Open a workspace or add linked project roots first.';
}
const grouped = new Map<string, AgentDefinition[]>();
for (const agent of agents) {
const bucket = grouped.get(agent.projectName) ?? [];
bucket.push(agent);
grouped.set(agent.projectName, bucket);
}
const lines: string[] = ['# Linked agents'];
for (const [projectName, projectAgents] of [...grouped.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
lines.push(`## ${projectName}`);
for (const agent of projectAgents.sort((left, right) => left.role.localeCompare(right.role))) {
lines.push(`- \`${agent.role}\` - ${agent.title} (\`${agent.relativePath}\`)`);
}
}
return lines.join('\n');
}
function buildGroundingPrompt(
agents: AgentDefinition[],
preferredProjectRoot: string | undefined,
selectedAgent: AgentDefinition | undefined
): string {
const projectAgents = preferredProjectRoot
? agents.filter(agent => agent.projectRoot === preferredProjectRoot)
: agents;
const roster = (projectAgents.length ? projectAgents : agents)
.map(agent => `- ${agent.role}: ${agent.title} (${agent.projectName} / ${agent.relativePath})`)
.join('\n');
const projectLine = preferredProjectRoot
? `Active project root: ${preferredProjectRoot}`
: 'No active project root detected.';
const agentBlock = selectedAgent
? `Selected agent prompt:\n${selectedAgent.body}`
: 'No single agent command was selected. Act as an operational cockpit coordinating runbooks, QA, firmware, hardware, and documentation for live execution.';
return [
'You are Kill LIFE Operator inside VS Code.',
'You focus on execution, status, incidents, runbooks, checks, and the smallest reliable next operational action.',
'Ground every answer in the linked local project agents listed below.',
'Do not invent files, commands, standards, or project state that were not provided.',
'If information is missing, say what is missing and ask for the smallest useful next input.',
projectLine,
'Available agents:',
roster || '- none',
agentBlock
].join('\n\n');
}
function extractHistory(context: vscode.ChatContext): vscode.LanguageModelChatMessage[] {
const previousMessages = context.history.filter(
historyTurn => historyTurn instanceof vscode.ChatResponseTurn
);
const messages: vscode.LanguageModelChatMessage[] = [];
for (const previousMessage of previousMessages) {
let fullMessage = '';
previousMessage.response.forEach(part => {
const markdownPart = part as vscode.ChatResponseMarkdownPart;
fullMessage += markdownPart.value.value;
});
if (fullMessage.trim()) {
messages.push(vscode.LanguageModelChatMessage.Assistant(fullMessage));
}
}
return messages;
}
async function pickAgent(
catalog: AgentCatalog,
placeHolder: string
): Promise<AgentDefinition | undefined> {
await catalog.ensureLoaded();
const agents = catalog.getAgents();
if (!agents.length) {
void vscode.window.showWarningMessage('No agents discovered yet.');
return undefined;
}
const picked = await vscode.window.showQuickPick(
agents.map(agent => ({
label: agent.title,
description: `${agent.projectName}${agent.role}`,
detail: agent.relativePath,
agent
})),
{ placeHolder }
);
return picked?.agent;
}
async function openAgentFile(agent: AgentDefinition): Promise<void> {
await vscode.window.showTextDocument(agent.file, {
preview: false
});
}
async function copyAgentPrompt(agent: AgentDefinition): Promise<void> {
await vscode.env.clipboard.writeText(agent.body);
void vscode.window.showInformationMessage(`Copied prompt: ${agent.title}`);
}
async function insertAgentPrompt(agent: AgentDefinition): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
const document = await vscode.workspace.openTextDocument({
content: agent.body,
language: 'markdown'
});
await vscode.window.showTextDocument(document, { preview: false });
return;
}
await editor.edit(editBuilder => {
editBuilder.insert(editor.selection.active, agent.body);
});
}
export function activate(context: vscode.ExtensionContext): void {
const catalog = new AgentCatalog();
const treeProvider = new AgentsTreeProvider(catalog);
context.subscriptions.push(catalog, treeProvider);
context.subscriptions.push(
vscode.window.registerTreeDataProvider('killLifeOperator.agentsView', treeProvider)
);
context.subscriptions.push(
vscode.commands.registerCommand('killLifeOperator.refreshAgents', async () => {
await catalog.refresh();
})
);
context.subscriptions.push(
vscode.commands.registerCommand('killLifeOperator.addLinkedProjectRoot', async () => {
await addLinkedProjectRoot();
await catalog.refresh();
})
);
context.subscriptions.push(
vscode.commands.registerCommand('killLifeOperator.removeLinkedProjectRoot', async () => {
await removeLinkedProjectRoot();
await catalog.refresh();
})
);
context.subscriptions.push(
vscode.commands.registerCommand('killLifeOperator.openAgentFile', async (agent?: AgentDefinition) => {
const resolvedAgent = agent ?? (await pickAgent(catalog, 'Select an agent file to open'));
if (resolvedAgent) {
await openAgentFile(resolvedAgent);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('killLifeOperator.copyAgentPrompt', async (agent?: AgentDefinition) => {
const resolvedAgent = agent ?? (await pickAgent(catalog, 'Select an agent prompt to copy'));
if (resolvedAgent) {
await copyAgentPrompt(resolvedAgent);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('killLifeOperator.insertAgentPrompt', async (agent?: AgentDefinition) => {
const resolvedAgent = agent ?? (await pickAgent(catalog, 'Select an agent prompt to insert'));
if (resolvedAgent) {
await insertAgentPrompt(resolvedAgent);
}
})
);
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('killLifeOperator')) {
void catalog.refresh();
}
})
);
context.subscriptions.push(
vscode.workspace.onDidChangeWorkspaceFolders(() => {
void catalog.refresh();
})
);
const chatHandler: vscode.ChatRequestHandler = async (
request,
chatContext,
stream,
token
) => {
await catalog.ensureLoaded();
const agents = catalog.getAgents();
if (request.command === 'agents') {
stream.markdown(buildRosterMarkdown(agents));
return;
}
if (!agents.length) {
stream.markdown(
'No agents discovered yet. Open a project workspace or run `Kill LIFE Operator: Add Linked Project Root` first.'
);
return;
}
const projectRoots = getAllProjectRoots();
const preferredProjectRoot = resolveProjectRootForActiveEditor(projectRoots);
const selectedRole = request.command && ROLE_COMMANDS.has(request.command) ? request.command : undefined;
const selectedAgent = selectedRole
? selectAgentForRole(agents, selectedRole, preferredProjectRoot)
: undefined;
const messages: vscode.LanguageModelChatMessage[] = [
vscode.LanguageModelChatMessage.User(
buildGroundingPrompt(agents, preferredProjectRoot, selectedAgent)
),
...extractHistory(chatContext),
vscode.LanguageModelChatMessage.User(
request.prompt.trim() || 'Provide a concise operational next-step plan for the selected agent.'
)
];
const response = await request.model.sendRequest(messages, {}, token);
for await (const fragment of response.text) {
stream.markdown(fragment);
}
};
const participant = vscode.chat.createChatParticipant(PARTICIPANT_ID, chatHandler);
participant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'kill-life.svg');
context.subscriptions.push(participant);
void catalog.refresh();
}
export function deactivate(): void {}
+99
View File
@@ -0,0 +1,99 @@
import * as vscode from 'vscode';
import { AgentCatalog, AgentDefinition, ProjectSummary } from './agentCatalog';
class ProjectNode {
readonly kind = 'project';
constructor(readonly project: ProjectSummary) {}
}
class AgentNode {
readonly kind = 'agent';
constructor(readonly agent: AgentDefinition) {}
}
class InfoNode {
readonly kind = 'info';
constructor(readonly message: string) {}
}
export type AgentsTreeNode = ProjectNode | AgentNode | InfoNode;
export class AgentsTreeProvider implements vscode.TreeDataProvider<AgentsTreeNode> {
private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter<AgentsTreeNode | undefined>();
readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
constructor(private readonly catalog: AgentCatalog) {
this.catalog.onDidChange(() => this.refresh());
}
refresh(): void {
this.onDidChangeTreeDataEmitter.fire(undefined);
}
getTreeItem(element: AgentsTreeNode): vscode.TreeItem {
if (element.kind === 'info') {
const infoItem = new vscode.TreeItem(element.message, vscode.TreeItemCollapsibleState.None);
infoItem.contextValue = 'infoItem';
infoItem.iconPath = new vscode.ThemeIcon('info');
return infoItem;
}
if (element.kind === 'project') {
const projectItem = new vscode.TreeItem(
element.project.name,
vscode.TreeItemCollapsibleState.Expanded
);
projectItem.description = `${element.project.agents.length} agent${element.project.agents.length > 1 ? 's' : ''}`;
projectItem.tooltip = element.project.root;
projectItem.contextValue = 'projectItem';
projectItem.iconPath = new vscode.ThemeIcon('repo');
return projectItem;
}
const agent = element.agent;
const agentItem = new vscode.TreeItem(agent.title, vscode.TreeItemCollapsibleState.None);
agentItem.description = agent.role;
agentItem.tooltip = `${agent.description}\n${agent.file.fsPath}`;
agentItem.resourceUri = agent.file;
agentItem.contextValue = 'agentItem';
agentItem.iconPath = new vscode.ThemeIcon('hubot');
agentItem.command = {
command: 'killLifeOperator.openAgentFile',
title: 'Open Agent File',
arguments: [agent]
};
return agentItem;
}
getChildren(element?: AgentsTreeNode): vscode.ProviderResult<AgentsTreeNode[]> {
if (!element) {
const projects = this.catalog.getProjects();
if (!projects.length) {
const error = this.catalog.getLastError();
if (error) {
return [new InfoNode(`Unable to load agents: ${error}`)];
}
return [
new InfoNode('Open a project workspace or add linked project roots to discover agents.')
];
}
return projects.map(project => new ProjectNode(project));
}
if (element.kind === 'project') {
return element.project.agents.map(agent => new AgentNode(agent));
}
return [];
}
dispose(): void {
this.onDidChangeTreeDataEmitter.dispose();
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": [
"ES2022"
],
"outDir": "out",
"rootDir": "src",
"strict": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"types": [
"node",
"vscode"
]
},
"include": [
"src/**/*.ts"
]
}