sistema-multiagente-sdlc 1.3.0 → 1.4.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 +21 -0
- package/README.md +23 -1
- package/migrations/1.4.0/README.md +8 -0
- package/migrations/1.4.0/up.mjs +14 -0
- package/package.json +1 -1
- package/src/cli.js +28 -2
- package/src/migrations.js +3 -1
- package/src/render.js +1 -1
- package/src/runtime.js +476 -0
- package/templates/.agents/skills/continua/SKILL.md +8 -0
- package/templates/.agents/skills/resume/SKILL.md +8 -0
- package/templates/.agents/skills/save/SKILL.md +8 -0
- package/templates/.claude/skills/continua/SKILL.md +18 -0
- package/templates/.claude/skills/resume/SKILL.md +18 -0
- package/templates/.claude/skills/save/SKILL.md +18 -0
- package/templates/.github/skills/continua/SKILL.md +18 -0
- package/templates/.github/skills/resume/SKILL.md +18 -0
- package/templates/.github/skills/save/SKILL.md +18 -0
- package/templates/.sdlc/session.json +8 -0
- package/templates/.windsurf/skills/continua/SKILL.md +8 -0
- package/templates/.windsurf/skills/resume/SKILL.md +8 -0
- package/templates/.windsurf/skills/save/SKILL.md +8 -0
- package/templates/manifest.yaml +28 -0
- package/templates/scripts/agent-skills.manifest.json +3 -0
- package/templates/scripts/continua.mjs +119 -0
- package/templates/scripts/continua.ps1 +11 -121
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
_No hay cambios pendientes._
|
|
6
6
|
|
|
7
|
+
## [1.4.0] — 2026-05-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- ADR `0004-codegraph-graphify-orden-canonico.md`: cierra la decisión pendiente de ADR 0002 y canoniza CodeGraph para estructura de código + Graphify para semántica documental/export Obsidian.
|
|
12
|
+
- Runtime Node multiagente como interfaz canónica: `sdlc session-start`, `resume`, `save`, `continua`, `memory-sync`, `validate-runtime` y `hooks install --post-merge-checkpoint`.
|
|
13
|
+
- `.sdlc/session.json` como estado generado de sesión para healthcheck y continuidad cross-IDE.
|
|
14
|
+
- Skills canónicas `resume`, `save` y `continua` bajo `.github/skills/` y mirrors para `.claude/`, `.agents/` y `.windsurf/`, todas apuntando al mismo CLI `sdlc`.
|
|
15
|
+
- `templates/scripts/continua.mjs` como implementación portable Node de continuidad; `continua.ps1` queda como wrapper Windows delgado.
|
|
16
|
+
- Migración `1.4.0` con marcador `.sdlc/migrations/1.4.0-applied.txt`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `sdlc doctor` y el runner de regresión validan la versión `1.4.0`.
|
|
21
|
+
- El manifest de skills gobernadas incluye `resume`, `save` y `continua`.
|
|
22
|
+
- La continuidad multiagente deja de depender de PowerShell como runtime primario; PowerShell queda para compatibilidad en Windows.
|
|
23
|
+
|
|
24
|
+
### Tests
|
|
25
|
+
|
|
26
|
+
- Regresión extendida con smoke tests de `session-start`, `resume`, `save --no-mutate`, `continua`, `memory-sync --mode health`, `validate-runtime` y `hooks install`.
|
|
27
|
+
|
|
7
28
|
## [1.3.0] — 2026-05-23
|
|
8
29
|
|
|
9
30
|
### Added
|
package/README.md
CHANGED
|
@@ -47,6 +47,28 @@ node ./bin/sdlc.js install --target ../legacy-project --mode legacy --project-na
|
|
|
47
47
|
node ./bin/sdlc.js doctor --target ../legacy-project --json
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Runtime Multiagente
|
|
51
|
+
|
|
52
|
+
Desde `1.4.0`, `sdlc` incluye comandos ejecutables para continuidad cross-IDE. El runtime primario es Node; los wrappers PowerShell solo existen para ergonomia Windows.
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
sdlc session-start --target . --json
|
|
56
|
+
sdlc resume --target . --markdown
|
|
57
|
+
sdlc save --target . --event manual --json
|
|
58
|
+
sdlc continua --target . --platform codex --json
|
|
59
|
+
sdlc memory-sync --target . --mode health --json
|
|
60
|
+
sdlc validate-runtime --target . --json
|
|
61
|
+
sdlc hooks install --target . --post-merge-checkpoint --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Reglas base:
|
|
65
|
+
|
|
66
|
+
- `session-start` crea `.sdlc/session.json` con healthcheck de Headroom, CodeGraph, Graphify, caveman, vault y slice actual.
|
|
67
|
+
- `resume` es solo lectura y recompone contexto en orden repo -> CodeGraph -> Graphify -> vault.
|
|
68
|
+
- `save` escribe checkpoints locales en el vault; no promueve GitHub Issues, OpenSpec ni PRs sin gate humano.
|
|
69
|
+
- `hooks install --post-merge-checkpoint` instala un hook local `post-merge` que ejecuta `sdlc save --event post-merge`.
|
|
70
|
+
- `memory-sync --mode nightly --apply` importa chats y exporta Graphify al vault; no crea checkpoints automaticos.
|
|
71
|
+
|
|
50
72
|
## Modes
|
|
51
73
|
|
|
52
74
|
| Mode | Use when | Adds |
|
|
@@ -124,7 +146,7 @@ All external installs are opt-in. Scripts default to dry-run or local-only behav
|
|
|
124
146
|
|
|
125
147
|
Side-by-side de los dos frameworks. La intención no es competir sino aclarar dónde se solapan y dónde cada uno se especializa. Datos de BMAD tomados de su README oficial v6 (`bmad-code-org/BMAD-METHOD`, npm `bmad-method`).
|
|
126
148
|
|
|
127
|
-
| Feature | BMAD-METHOD v6 | SistemaMultiagente_SDLC v1.
|
|
149
|
+
| Feature | BMAD-METHOD v6 | SistemaMultiagente_SDLC v1.4.0 |
|
|
128
150
|
| --- | --- | --- |
|
|
129
151
|
| License | MIT | MIT |
|
|
130
152
|
| Runtime requisitos | Node ≥20.12, Python ≥3.10, `uv` | Node ≥18, PowerShell (pwsh/powershell), Git |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Migration 1.4.0
|
|
2
|
+
|
|
3
|
+
Adds the executable runtime layer for multi-agent continuity:
|
|
4
|
+
|
|
5
|
+
- Node-first `sdlc` runtime commands.
|
|
6
|
+
- Generated `.sdlc/session.json`.
|
|
7
|
+
- Cross-IDE `resume`, `save`, and `continua` skills.
|
|
8
|
+
- Thin PowerShell wrappers for Windows convenience.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function up(files = {}) {
|
|
2
|
+
const extra = {
|
|
3
|
+
".sdlc/migrations/1.4.0-applied.txt": "Migration 1.4.0 applied by SistemaMultiagente_SDLC.\ngenerated-by-sdlc\n"
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const configPath = ".sdlc/config.json";
|
|
7
|
+
if (typeof files[configPath] === "string") {
|
|
8
|
+
const config = JSON.parse(files[configPath]);
|
|
9
|
+
config.frameworkVersion = "1.4.0";
|
|
10
|
+
extra[configPath] = `${JSON.stringify(config, null, 2)}\n`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return extra;
|
|
14
|
+
}
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,6 +17,15 @@ import {
|
|
|
17
17
|
} from "./file-utils.js";
|
|
18
18
|
import { buildManagedFiles, defaultConfig, FRAMEWORK_VERSION, validateConfigShape } from "./render.js";
|
|
19
19
|
import { applyMigrations, migrationsToRun, SUPPORTED_VERSIONS } from "./migrations.js";
|
|
20
|
+
import {
|
|
21
|
+
commandContinua,
|
|
22
|
+
commandHooks,
|
|
23
|
+
commandMemorySync,
|
|
24
|
+
commandResume,
|
|
25
|
+
commandSave,
|
|
26
|
+
commandSessionStart,
|
|
27
|
+
commandValidateRuntime
|
|
28
|
+
} from "./runtime.js";
|
|
20
29
|
|
|
21
30
|
const EXIT_OK = 0;
|
|
22
31
|
const EXIT_ERROR = 1;
|
|
@@ -44,7 +53,10 @@ function parseArgs(argv) {
|
|
|
44
53
|
}
|
|
45
54
|
positionals.push(token);
|
|
46
55
|
}
|
|
47
|
-
result.
|
|
56
|
+
result.positionals = positionals;
|
|
57
|
+
result.command = positionals[0] === "hooks" && positionals[1] === "install"
|
|
58
|
+
? "hooks install"
|
|
59
|
+
: positionals[0] ?? "help";
|
|
48
60
|
return result;
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -622,7 +634,7 @@ function commandHelp() {
|
|
|
622
634
|
exitCode: EXIT_OK,
|
|
623
635
|
payload: {
|
|
624
636
|
status: "ok",
|
|
625
|
-
message: "Uso: sdlc <init|install|upgrade|rollback|doctor|diff|prune-backups|migrate-config> [--target <repo>] [--json]\nSi --target se omite, se usa el directorio actual (process.cwd())."
|
|
637
|
+
message: "Uso: sdlc <init|install|upgrade|rollback|doctor|diff|prune-backups|migrate-config|session-start|resume|save|continua|memory-sync|validate-runtime|hooks install> [--target <repo>] [--json]\nSi --target se omite, se usa el directorio actual (process.cwd())."
|
|
626
638
|
}
|
|
627
639
|
};
|
|
628
640
|
}
|
|
@@ -645,6 +657,20 @@ export function run(argv) {
|
|
|
645
657
|
return commandPruneBackups(parsed.options);
|
|
646
658
|
case "migrate-config":
|
|
647
659
|
return commandMigrateConfig(parsed.options);
|
|
660
|
+
case "session-start":
|
|
661
|
+
return commandSessionStart(parsed.options);
|
|
662
|
+
case "resume":
|
|
663
|
+
return commandResume(parsed.options);
|
|
664
|
+
case "save":
|
|
665
|
+
return commandSave(parsed.options);
|
|
666
|
+
case "continua":
|
|
667
|
+
return commandContinua(parsed.options);
|
|
668
|
+
case "memory-sync":
|
|
669
|
+
return commandMemorySync(parsed.options);
|
|
670
|
+
case "validate-runtime":
|
|
671
|
+
return commandValidateRuntime(parsed.options);
|
|
672
|
+
case "hooks install":
|
|
673
|
+
return commandHooks(parsed.options);
|
|
648
674
|
case "help":
|
|
649
675
|
default:
|
|
650
676
|
return commandHelp();
|
package/src/migrations.js
CHANGED
|
@@ -2,12 +2,14 @@ import { up as up_1_0_1 } from "../migrations/1.0.1/up.mjs";
|
|
|
2
2
|
import { up as up_1_1_0 } from "../migrations/1.1.0/up.mjs";
|
|
3
3
|
import { up as up_1_2_0 } from "../migrations/1.2.0/up.mjs";
|
|
4
4
|
import { up as up_1_3_0 } from "../migrations/1.3.0/up.mjs";
|
|
5
|
+
import { up as up_1_4_0 } from "../migrations/1.4.0/up.mjs";
|
|
5
6
|
|
|
6
7
|
const REGISTRY = [
|
|
7
8
|
{ version: "1.0.1", up: up_1_0_1 },
|
|
8
9
|
{ version: "1.1.0", up: up_1_1_0 },
|
|
9
10
|
{ version: "1.2.0", up: up_1_2_0 },
|
|
10
|
-
{ version: "1.3.0", up: up_1_3_0 }
|
|
11
|
+
{ version: "1.3.0", up: up_1_3_0 },
|
|
12
|
+
{ version: "1.4.0", up: up_1_4_0 }
|
|
11
13
|
];
|
|
12
14
|
|
|
13
15
|
function semverTuple(v) {
|
package/src/render.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { renderTemplates } from "./template-loader.js";
|
|
3
3
|
export { validateConfigShape } from "./config-validator.js";
|
|
4
4
|
|
|
5
|
-
export const FRAMEWORK_VERSION = "1.
|
|
5
|
+
export const FRAMEWORK_VERSION = "1.4.0";
|
|
6
6
|
export const SCHEMA_VERSION = 1;
|
|
7
7
|
export const SUPPORTED_MODES = new Set(["greenfield", "legacy"]);
|
|
8
8
|
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { ensureDir, pathExists, readJson, readTextIfExists, writeJson, writeText } from "./file-utils.js";
|
|
6
|
+
|
|
7
|
+
const EXIT_OK = 0;
|
|
8
|
+
const EXIT_ERROR = 1;
|
|
9
|
+
const EXIT_ACTION_REQUIRED = 2;
|
|
10
|
+
|
|
11
|
+
function nowIso() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function expandEnv(value) {
|
|
16
|
+
if (!value || typeof value !== "string") return value;
|
|
17
|
+
return value
|
|
18
|
+
.replace(/%([^%]+)%/g, (_, name) => process.env[name] ?? `%${name}%`)
|
|
19
|
+
.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? `\${${name}}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeReadJson(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
return pathExists(filePath) ? readJson(filePath) : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getConfig(target) {
|
|
31
|
+
return safeReadJson(path.join(target, ".sdlc", "config.json")) ?? {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getProjectSlug(target, config = getConfig(target)) {
|
|
35
|
+
return config.project?.slug || path.basename(path.resolve(target)).toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runCommand(command, args = [], cwd = process.cwd(), timeout = 8000) {
|
|
39
|
+
const windowsShell = process.platform === "win32";
|
|
40
|
+
const quoteWindowsArg = (value) => {
|
|
41
|
+
const text = String(value);
|
|
42
|
+
return /[\s"&|<>^]/.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
|
43
|
+
};
|
|
44
|
+
const result = windowsShell
|
|
45
|
+
? spawnSync([command, ...args].map(quoteWindowsArg).join(" "), {
|
|
46
|
+
cwd,
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
shell: true,
|
|
49
|
+
timeout
|
|
50
|
+
})
|
|
51
|
+
: spawnSync(command, args, {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
ok: result.status === 0,
|
|
58
|
+
status: result.status,
|
|
59
|
+
stdout: (result.stdout ?? "").trim(),
|
|
60
|
+
stderr: (result.stderr ?? "").trim(),
|
|
61
|
+
error: result.error?.message
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkHttp(url, timeoutMs = 1200) {
|
|
66
|
+
const code = `
|
|
67
|
+
const url = ${JSON.stringify(url)};
|
|
68
|
+
const timeout = ${Number(timeoutMs)};
|
|
69
|
+
const signal = AbortSignal.timeout(timeout);
|
|
70
|
+
fetch(url, { signal })
|
|
71
|
+
.then((res) => process.exit(res.ok ? 0 : 2))
|
|
72
|
+
.catch(() => process.exit(1));
|
|
73
|
+
`;
|
|
74
|
+
const result = spawnSync(process.execPath, ["-e", code], { encoding: "utf8", timeout: timeoutMs + 1000 });
|
|
75
|
+
return result.status === 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fileAgeHours(filePath) {
|
|
79
|
+
if (!pathExists(filePath)) return null;
|
|
80
|
+
return Math.round(((Date.now() - fs.statSync(filePath).mtimeMs) / 36_000) / 100) / 100;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function latestFile(root, extension = ".md") {
|
|
84
|
+
if (!pathExists(root)) return null;
|
|
85
|
+
const files = [];
|
|
86
|
+
const walk = (current) => {
|
|
87
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
88
|
+
const absolute = path.join(current, entry.name);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
walk(absolute);
|
|
91
|
+
} else if (entry.name.endsWith(extension)) {
|
|
92
|
+
files.push(absolute);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(root);
|
|
97
|
+
return files
|
|
98
|
+
.map((file) => ({ file, mtime: fs.statSync(file).mtimeMs }))
|
|
99
|
+
.sort((a, b) => b.mtime - a.mtime)[0]?.file ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function preview(text, max = 900) {
|
|
103
|
+
if (!text) return null;
|
|
104
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
105
|
+
return clean.length > max ? `${clean.slice(0, max).trim()}...` : clean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveMemoryConfig(target) {
|
|
109
|
+
const localPath = path.join(target, "scripts", "obsidian-memory.config.local.json");
|
|
110
|
+
const examplePath = path.join(target, "scripts", "obsidian-memory.config.example.json");
|
|
111
|
+
const configPath = pathExists(localPath) ? localPath : pathExists(examplePath) ? examplePath : null;
|
|
112
|
+
const config = configPath ? safeReadJson(configPath) : null;
|
|
113
|
+
const projectSlug = config?.projectSlug || getProjectSlug(target);
|
|
114
|
+
const rawVault = config?.vaultRoot || config?.obsidian?.vaultPath;
|
|
115
|
+
const vaultRoot = rawVault && !String(rawVault).includes("{{") ? path.resolve(expandEnv(String(rawVault))) : path.join(target, ".sdlc", "vault");
|
|
116
|
+
return {
|
|
117
|
+
configPath,
|
|
118
|
+
config,
|
|
119
|
+
projectSlug,
|
|
120
|
+
vaultRoot,
|
|
121
|
+
projectRoot: path.join(vaultRoot, projectSlug),
|
|
122
|
+
checkpointsDir: path.join(vaultRoot, projectSlug, "checkpoints"),
|
|
123
|
+
syncLogsDir: path.join(vaultRoot, projectSlug, "logs", "sync"),
|
|
124
|
+
graphifyDir: config?.graphifyObsidianDir ? path.resolve(expandEnv(String(config.graphifyObsidianDir))) : path.join(vaultRoot, "graphify", projectSlug)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readState(target) {
|
|
129
|
+
const currentSlicePath = path.join(target, ".github", "agent-state", "current-slice.md");
|
|
130
|
+
const activeSlicesPath = path.join(target, ".github", "agent-state", "active-slices.yaml");
|
|
131
|
+
const phaseStatusPath = path.join(target, ".github", "agent-state", "phase-status.yaml");
|
|
132
|
+
const currentSlice = readTextIfExists(currentSlicePath);
|
|
133
|
+
const activeSlices = readTextIfExists(activeSlicesPath);
|
|
134
|
+
const phaseStatus = readTextIfExists(phaseStatusPath);
|
|
135
|
+
const sliceId = currentSlice?.match(/`([^`]+)`/)?.[1] ?? "unknown";
|
|
136
|
+
const phase = currentSlice?.match(/SDLC Phase\s*\n\s*-\s*`([^`]+)`/i)?.[1] ?? currentSlice?.match(/\bF\d+(?:\.\d+)?\b/)?.[0] ?? "unknown";
|
|
137
|
+
return {
|
|
138
|
+
currentSlicePath,
|
|
139
|
+
activeSlicesPath,
|
|
140
|
+
phaseStatusPath,
|
|
141
|
+
sliceId,
|
|
142
|
+
phase,
|
|
143
|
+
currentSlicePreview: preview(currentSlice),
|
|
144
|
+
activeSlicesPreview: preview(activeSlices),
|
|
145
|
+
phaseStatusPreview: preview(phaseStatus),
|
|
146
|
+
activeSliceDeclared: Boolean(activeSlices && /active:\s*\n\s*-\s*/.test(activeSlices))
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function schedulerHeadroomTask() {
|
|
151
|
+
if (process.platform !== "win32") {
|
|
152
|
+
return { supported: false, exists: false, taskName: null };
|
|
153
|
+
}
|
|
154
|
+
const ps = [
|
|
155
|
+
"-NoProfile",
|
|
156
|
+
"-Command",
|
|
157
|
+
"Get-ScheduledTask | Where-Object { $_.TaskName -match 'Headroom' } | Select-Object -First 1 -ExpandProperty TaskName"
|
|
158
|
+
];
|
|
159
|
+
const result = spawnSync("powershell.exe", ps, { encoding: "utf8", timeout: 5000 });
|
|
160
|
+
const stdout = (result.stdout ?? "").trim();
|
|
161
|
+
return { supported: true, exists: result.status === 0 && Boolean(stdout), taskName: stdout || null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function collectRuntime(target) {
|
|
165
|
+
const config = getConfig(target);
|
|
166
|
+
const memory = resolveMemoryConfig(target);
|
|
167
|
+
const state = readState(target);
|
|
168
|
+
const claudeSettings = safeReadJson(path.join(os.homedir(), ".claude", "settings.json"));
|
|
169
|
+
const claudeSettingsText = claudeSettings ? JSON.stringify(claudeSettings) : "";
|
|
170
|
+
const codegraphDb = path.join(target, ".codegraph", "codegraph.db");
|
|
171
|
+
const codegraphStatus = pathExists(path.join(target, ".codegraph", "config.json")) ? runCommand("codegraph", ["status"], target, 12_000) : null;
|
|
172
|
+
const graphManifest = path.join(target, "graphify-out", "manifest.json");
|
|
173
|
+
const graphReport = path.join(target, "graphify-out", "GRAPH_REPORT.md");
|
|
174
|
+
const gitBranch = runCommand("git", ["branch", "--show-current"], target, 5000);
|
|
175
|
+
const gitHead = runCommand("git", ["log", "-1", "--oneline"], target, 5000);
|
|
176
|
+
const scheduler = schedulerHeadroomTask();
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
generatedAt: nowIso(),
|
|
180
|
+
target,
|
|
181
|
+
project: getProjectSlug(target, config),
|
|
182
|
+
git: {
|
|
183
|
+
branch: gitBranch.ok ? gitBranch.stdout : null,
|
|
184
|
+
head: gitHead.ok ? gitHead.stdout : null
|
|
185
|
+
},
|
|
186
|
+
state,
|
|
187
|
+
headroom: {
|
|
188
|
+
healthUrl: "http://127.0.0.1:8787/health",
|
|
189
|
+
healthy: checkHttp("http://127.0.0.1:8787/health"),
|
|
190
|
+
claudeBaseUrl: claudeSettings?.env?.ANTHROPIC_BASE_URL ?? null,
|
|
191
|
+
hookDetected: /headroom\s+init\s+hook\s+ensure/i.test(claudeSettingsText),
|
|
192
|
+
scheduler
|
|
193
|
+
},
|
|
194
|
+
caveman: {
|
|
195
|
+
hookDetected: /caveman-activate\.js/i.test(claudeSettingsText),
|
|
196
|
+
trackerDetected: /caveman-mode-tracker\.js/i.test(claudeSettingsText),
|
|
197
|
+
flag: readTextIfExists(path.join(os.homedir(), ".claude", ".caveman-active"))?.trim() ?? null
|
|
198
|
+
},
|
|
199
|
+
codegraph: {
|
|
200
|
+
configured: pathExists(path.join(target, ".codegraph", "config.json")),
|
|
201
|
+
dbPath: pathExists(codegraphDb) ? codegraphDb : null,
|
|
202
|
+
dbAgeHours: fileAgeHours(codegraphDb),
|
|
203
|
+
statusOk: codegraphStatus ? codegraphStatus.ok : false,
|
|
204
|
+
statusPreview: codegraphStatus ? preview(codegraphStatus.stdout || codegraphStatus.stderr, 500) : null
|
|
205
|
+
},
|
|
206
|
+
graphify: {
|
|
207
|
+
graphPath: pathExists(path.join(target, "graphify-out", "graph.json")) ? path.join(target, "graphify-out", "graph.json") : null,
|
|
208
|
+
manifestAgeHours: fileAgeHours(graphManifest),
|
|
209
|
+
reportAgeHours: fileAgeHours(graphReport)
|
|
210
|
+
},
|
|
211
|
+
vault: {
|
|
212
|
+
configPath: memory.configPath,
|
|
213
|
+
root: memory.vaultRoot,
|
|
214
|
+
exists: pathExists(memory.vaultRoot),
|
|
215
|
+
latestCheckpoint: latestFile(memory.checkpointsDir) ?? latestFile(path.join(memory.projectRoot, "logs"))
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function lazyRefreshCodeGraph(target, runtime) {
|
|
221
|
+
if (!runtime.codegraph.configured || runtime.codegraph.statusOk) {
|
|
222
|
+
return { attempted: false, reason: runtime.codegraph.configured ? "status-ok" : "not-configured" };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = runCommand("codegraph", ["sync", "."], target, 60_000);
|
|
226
|
+
return {
|
|
227
|
+
attempted: true,
|
|
228
|
+
ok: result.ok,
|
|
229
|
+
status: result.status,
|
|
230
|
+
preview: preview(result.stdout || result.stderr, 500)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function runtimeFindings(runtime) {
|
|
235
|
+
const findings = [];
|
|
236
|
+
const add = (level, code, message) => findings.push({ level, code, message });
|
|
237
|
+
if (!runtime.headroom.hookDetected) add("warning", "headroom-hook-missing", "Claude headroom hook not detected.");
|
|
238
|
+
if (!runtime.headroom.scheduler.exists) add("warning", "headroom-scheduler-missing", "Headroom Scheduler fallback not detected.");
|
|
239
|
+
if (!runtime.caveman.hookDetected) add("warning", "caveman-hook-missing", "Caveman activation hook not detected.");
|
|
240
|
+
if (runtime.codegraph.configured && !runtime.codegraph.statusOk) add("warning", "codegraph-status", "CodeGraph configured but status check failed.");
|
|
241
|
+
if (runtime.graphify.manifestAgeHours !== null && runtime.graphify.manifestAgeHours > 24) add("warning", "graphify-stale", `Graphify manifest age ${runtime.graphify.manifestAgeHours}h.`);
|
|
242
|
+
if (!runtime.vault.exists) add("warning", "vault-missing", `Vault path not found: ${runtime.vault.root}`);
|
|
243
|
+
if (runtime.state.sliceId !== "unknown" && !runtime.state.activeSliceDeclared) add("warning", "active-slices-empty", "current-slice exists but active-slices.yaml has no active entry.");
|
|
244
|
+
return findings;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function commandSessionStart(options) {
|
|
248
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
249
|
+
let runtime = collectRuntime(target);
|
|
250
|
+
const codegraphRefresh = lazyRefreshCodeGraph(target, runtime);
|
|
251
|
+
if (codegraphRefresh.attempted) {
|
|
252
|
+
runtime = collectRuntime(target);
|
|
253
|
+
}
|
|
254
|
+
runtime.codegraph.lazyRefresh = codegraphRefresh;
|
|
255
|
+
const findings = runtimeFindings(runtime);
|
|
256
|
+
const payload = {
|
|
257
|
+
status: findings.some((f) => f.level === "error") ? "error" : findings.length > 0 ? "warning" : "ok",
|
|
258
|
+
runtime,
|
|
259
|
+
findings
|
|
260
|
+
};
|
|
261
|
+
writeJson(path.join(target, ".sdlc", "session.json"), payload);
|
|
262
|
+
return { exitCode: EXIT_OK, payload };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function commandValidateRuntime(options) {
|
|
266
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
267
|
+
const runtime = collectRuntime(target);
|
|
268
|
+
const findings = runtimeFindings(runtime);
|
|
269
|
+
const hasErrors = findings.some((f) => f.level === "error");
|
|
270
|
+
const hasWarnings = findings.some((f) => f.level === "warning");
|
|
271
|
+
return {
|
|
272
|
+
exitCode: hasErrors ? EXIT_ERROR : hasWarnings ? EXIT_ACTION_REQUIRED : EXIT_OK,
|
|
273
|
+
payload: {
|
|
274
|
+
status: hasErrors ? "error" : hasWarnings ? "warning" : "ok",
|
|
275
|
+
findings,
|
|
276
|
+
runtime
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function commandResume(options) {
|
|
282
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
283
|
+
const runtime = collectRuntime(target);
|
|
284
|
+
const memory = resolveMemoryConfig(target);
|
|
285
|
+
const openSpecChanges = path.join(target, "openspec", "changes");
|
|
286
|
+
const activeChanges = pathExists(openSpecChanges)
|
|
287
|
+
? fs.readdirSync(openSpecChanges, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name)
|
|
288
|
+
: [];
|
|
289
|
+
const result = {
|
|
290
|
+
status: "ok",
|
|
291
|
+
ownerAgent: runtime.state.phase === "definition" ? "analista-requisitos-migracion" : "orquestador-opus",
|
|
292
|
+
sliceId: runtime.state.sliceId,
|
|
293
|
+
phase: runtime.state.phase,
|
|
294
|
+
branch: runtime.git.branch,
|
|
295
|
+
head: runtime.git.head,
|
|
296
|
+
activeChanges,
|
|
297
|
+
latestCheckpoint: runtime.vault.latestCheckpoint,
|
|
298
|
+
readinessStatus: "unknown",
|
|
299
|
+
promotionStatus: "draft-local",
|
|
300
|
+
nextCommand: runtime.state.phase === "definition" ? "/enrich-us o Continua con analista-requisitos-migracion" : "Continua",
|
|
301
|
+
runtimeSummary: {
|
|
302
|
+
headroomHealthy: runtime.headroom.healthy,
|
|
303
|
+
codegraphOk: runtime.codegraph.statusOk,
|
|
304
|
+
graphifyReportAgeHours: runtime.graphify.reportAgeHours,
|
|
305
|
+
vault: memory.vaultRoot
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
if (options.markdown) {
|
|
309
|
+
return {
|
|
310
|
+
exitCode: EXIT_OK,
|
|
311
|
+
payload: {
|
|
312
|
+
status: "ok",
|
|
313
|
+
message: [
|
|
314
|
+
"# SDLC Resume",
|
|
315
|
+
"",
|
|
316
|
+
`- owner-agent: ${result.ownerAgent}`,
|
|
317
|
+
`- slice-id: ${result.sliceId}`,
|
|
318
|
+
`- phase: ${result.phase}`,
|
|
319
|
+
`- branch: ${result.branch ?? "unknown"}`,
|
|
320
|
+
`- latest-checkpoint: ${result.latestCheckpoint ?? "none"}`,
|
|
321
|
+
`- next-command: ${result.nextCommand}`,
|
|
322
|
+
"",
|
|
323
|
+
"## Active OpenSpec changes",
|
|
324
|
+
"",
|
|
325
|
+
...(activeChanges.length ? activeChanges.map((change) => `- ${change}`) : ["- none"])
|
|
326
|
+
].join("\n")
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return { exitCode: EXIT_OK, payload: result };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function commandSave(options) {
|
|
334
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
335
|
+
const event = options.event ?? "manual";
|
|
336
|
+
const noMutate = Boolean(options["no-mutate"] || options["dry-run"]);
|
|
337
|
+
let runtime = collectRuntime(target);
|
|
338
|
+
const codegraphRefresh = lazyRefreshCodeGraph(target, runtime);
|
|
339
|
+
if (codegraphRefresh.attempted) {
|
|
340
|
+
runtime = collectRuntime(target);
|
|
341
|
+
}
|
|
342
|
+
runtime.codegraph.lazyRefresh = codegraphRefresh;
|
|
343
|
+
const memory = resolveMemoryConfig(target);
|
|
344
|
+
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 12);
|
|
345
|
+
const sliceSlug = runtime.state.sliceId === "unknown" ? "unknown" : runtime.state.sliceId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
346
|
+
const checkpointPath = path.join(memory.checkpointsDir, `${timestamp}-slice-${sliceSlug}.md`);
|
|
347
|
+
const diffStat = runCommand("git", ["diff", "--stat"], target, 5000);
|
|
348
|
+
const content = [
|
|
349
|
+
"---",
|
|
350
|
+
"generated_by: sdlc-save",
|
|
351
|
+
`event: ${event}`,
|
|
352
|
+
`created_at: ${nowIso()}`,
|
|
353
|
+
`slice: ${runtime.state.sliceId}`,
|
|
354
|
+
`phase: ${runtime.state.phase}`,
|
|
355
|
+
`branch: ${runtime.git.branch ?? "unknown"}`,
|
|
356
|
+
"promotion_status: draft-local",
|
|
357
|
+
"---",
|
|
358
|
+
"",
|
|
359
|
+
`# Checkpoint ${runtime.state.sliceId}`,
|
|
360
|
+
"",
|
|
361
|
+
"## Runtime",
|
|
362
|
+
"",
|
|
363
|
+
`- Headroom healthy: ${runtime.headroom.healthy}`,
|
|
364
|
+
`- CodeGraph OK: ${runtime.codegraph.statusOk}`,
|
|
365
|
+
`- Graphify report age hours: ${runtime.graphify.reportAgeHours ?? "unknown"}`,
|
|
366
|
+
"",
|
|
367
|
+
"## Git Diff Stat",
|
|
368
|
+
"",
|
|
369
|
+
"```text",
|
|
370
|
+
diffStat.stdout || "(no diff)",
|
|
371
|
+
"```",
|
|
372
|
+
"",
|
|
373
|
+
"## Next Command",
|
|
374
|
+
"",
|
|
375
|
+
runtime.state.phase === "definition" ? "Continua con analista-requisitos-migracion." : "Continua.",
|
|
376
|
+
""
|
|
377
|
+
].join("\n");
|
|
378
|
+
if (!noMutate) {
|
|
379
|
+
ensureDir(path.dirname(checkpointPath));
|
|
380
|
+
writeText(checkpointPath, content);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
exitCode: EXIT_OK,
|
|
384
|
+
payload: {
|
|
385
|
+
status: "ok",
|
|
386
|
+
dry_run: noMutate,
|
|
387
|
+
event,
|
|
388
|
+
checkpoint: checkpointPath
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function commandContinua(options) {
|
|
394
|
+
const session = commandSessionStart(options).payload;
|
|
395
|
+
const resume = commandResume(options).payload;
|
|
396
|
+
return {
|
|
397
|
+
exitCode: EXIT_OK,
|
|
398
|
+
payload: {
|
|
399
|
+
status: "ok",
|
|
400
|
+
platform: options.platform ?? "codex",
|
|
401
|
+
sessionStatus: session.status,
|
|
402
|
+
resume
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function commandMemorySync(options) {
|
|
408
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
409
|
+
const mode = options.mode ?? "health";
|
|
410
|
+
const apply = Boolean(options.apply);
|
|
411
|
+
const memory = resolveMemoryConfig(target);
|
|
412
|
+
const steps = [];
|
|
413
|
+
const logLines = [`[${nowIso()}] memory-sync mode=${mode} apply=${apply}`];
|
|
414
|
+
const addStep = (name, result) => {
|
|
415
|
+
steps.push({ name, ...result });
|
|
416
|
+
logLines.push(`[${nowIso()}] ${name}: ${result.status}`);
|
|
417
|
+
};
|
|
418
|
+
const configOk = Boolean(memory.configPath && pathExists(memory.configPath));
|
|
419
|
+
addStep("config", { status: configOk ? "ok" : "missing", path: memory.configPath });
|
|
420
|
+
if (mode === "health" || !apply) {
|
|
421
|
+
return { exitCode: configOk ? EXIT_OK : EXIT_ACTION_REQUIRED, payload: { status: configOk ? "ok" : "warning", dry_run: !apply, mode, steps } };
|
|
422
|
+
}
|
|
423
|
+
ensureDir(memory.syncLogsDir);
|
|
424
|
+
const logPath = path.join(memory.syncLogsDir, `${new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 12)}-${mode}.log`);
|
|
425
|
+
if (["sync", "nightly"].includes(mode)) {
|
|
426
|
+
const converter = path.join(target, "scripts", "claude-to-obsidian.py");
|
|
427
|
+
if (configOk && pathExists(converter)) {
|
|
428
|
+
const result = runCommand(options["python-exe"] ?? "python", [converter, "--config", memory.configPath], target, 60_000);
|
|
429
|
+
addStep("sync", { status: result.ok ? "ok" : "error", stdout: result.stdout, stderr: result.stderr });
|
|
430
|
+
} else {
|
|
431
|
+
addStep("sync", { status: "skipped", reason: "config or converter missing" });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (["export-graph", "nightly"].includes(mode)) {
|
|
435
|
+
const exporter = path.join(target, "scripts", "export-graphify-obsidian.py");
|
|
436
|
+
const graph = path.join(target, "graphify-out", "graph.json");
|
|
437
|
+
if (pathExists(exporter) && pathExists(graph)) {
|
|
438
|
+
const result = runCommand(options["python-exe"] ?? "python", [exporter, "--graph", graph, "--output-dir", memory.graphifyDir], target, 60_000);
|
|
439
|
+
addStep("export-graph", { status: result.ok ? "ok" : "error", stdout: result.stdout, stderr: result.stderr });
|
|
440
|
+
} else {
|
|
441
|
+
addStep("export-graph", { status: "skipped", reason: "graphify export inputs missing" });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
writeText(logPath, `${logLines.join("\n")}\n`);
|
|
445
|
+
const failed = steps.some((step) => step.status === "error");
|
|
446
|
+
return { exitCode: failed ? EXIT_ERROR : EXIT_OK, payload: { status: failed ? "error" : "ok", dry_run: false, mode, logPath, steps } };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function commandHooks(options) {
|
|
450
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
451
|
+
if (!options["post-merge-checkpoint"]) {
|
|
452
|
+
return { exitCode: EXIT_ERROR, payload: { status: "error", message: "Falta --post-merge-checkpoint" } };
|
|
453
|
+
}
|
|
454
|
+
const hookPath = path.join(target, ".git", "hooks", "post-merge");
|
|
455
|
+
const script = [
|
|
456
|
+
"#!/bin/sh",
|
|
457
|
+
"# generated by SistemaMultiagente_SDLC",
|
|
458
|
+
"target=\"$(pwd)\"",
|
|
459
|
+
"if command -v cygpath >/dev/null 2>&1; then",
|
|
460
|
+
" target=\"$(cygpath -w \"$target\")\"",
|
|
461
|
+
"elif pwd -W >/dev/null 2>&1; then",
|
|
462
|
+
" target=\"$(pwd -W)\"",
|
|
463
|
+
"fi",
|
|
464
|
+
"if command -v npx >/dev/null 2>&1; then",
|
|
465
|
+
" npx --no-install sdlc save --target \"$target\" --event post-merge --json >/dev/null 2>&1 || true",
|
|
466
|
+
"fi",
|
|
467
|
+
""
|
|
468
|
+
].join("\n");
|
|
469
|
+
writeText(hookPath, script);
|
|
470
|
+
try {
|
|
471
|
+
fs.chmodSync(hookPath, 0o755);
|
|
472
|
+
} catch {
|
|
473
|
+
// chmod is best-effort on Windows.
|
|
474
|
+
}
|
|
475
|
+
return { exitCode: EXIT_OK, payload: { status: "ok", hook: hookPath } };
|
|
476
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: continua
|
|
3
|
+
description: "Ejecuta session-start + resume para continuar un slice SDLC desde cualquier IDE. Usar cuando el usuario diga Continua."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Continua
|
|
7
|
+
|
|
8
|
+
Ejecutar:
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
npx --no-install sdlc continua --target . --platform claude_code --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Reglas:
|
|
15
|
+
|
|
16
|
+
- Primero valida runtime.
|
|
17
|
+
- Luego reconstruye contexto.
|
|
18
|
+
- Si hay gate humano pendiente, no implementar; reportar owner y bloqueo.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resume
|
|
3
|
+
description: "Reconstruye contexto SDLC sin mutar archivos usando el runtime Node de SistemaMultiagente_SDLC. Usar cuando el usuario invoque /resume o pida retomar contexto."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Resume
|
|
7
|
+
|
|
8
|
+
Ejecutar:
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
npx --no-install sdlc resume --target . --markdown
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Reglas:
|
|
15
|
+
|
|
16
|
+
- No modificar archivos.
|
|
17
|
+
- Respetar jerarquia repo -> CodeGraph -> Graphify -> vault.
|
|
18
|
+
- Si falta definicion funcional o readiness, devolver owner a `analista-requisitos-migracion`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: save
|
|
3
|
+
description: "Escribe un checkpoint portable en el vault local usando el runtime Node de SistemaMultiagente_SDLC. Usar cuando el usuario invoque /save o pida guardar continuidad."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Save
|
|
7
|
+
|
|
8
|
+
Ejecutar:
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
npx --no-install sdlc save --target . --event manual --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Reglas:
|
|
15
|
+
|
|
16
|
+
- Escribe checkpoint local en el vault.
|
|
17
|
+
- No promueve a GitHub Issue, OpenSpec ni PR sin gate humano.
|
|
18
|
+
- Marcar cualquier decision durable como pendiente de promocion al repo.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: continua
|
|
3
|
+
description: "Ejecuta session-start + resume para continuar un slice SDLC desde cualquier IDE. Usar cuando el usuario diga Continua."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Continua
|
|
7
|
+
|
|
8
|
+
Comando canónico:
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
npx --no-install sdlc continua --target . --platform claude_code --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Reglas:
|
|
15
|
+
|
|
16
|
+
- Primero valida runtime.
|
|
17
|
+
- Luego reconstruye contexto.
|
|
18
|
+
- Si hay gate humano pendiente, no implementar; reportar owner y bloqueo.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resume
|
|
3
|
+
description: "Reconstruye contexto SDLC sin mutar archivos usando el runtime Node de SistemaMultiagente_SDLC. Usar cuando el usuario invoque /resume o pida retomar contexto."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Resume
|
|
7
|
+
|
|
8
|
+
Comando canónico:
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
npx --no-install sdlc resume --target . --markdown
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Reglas:
|
|
15
|
+
|
|
16
|
+
- No modificar archivos.
|
|
17
|
+
- Respetar jerarquia repo -> CodeGraph -> Graphify -> vault.
|
|
18
|
+
- Si falta definicion funcional o readiness, devolver owner a `analista-requisitos-migracion`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: save
|
|
3
|
+
description: "Escribe un checkpoint portable en el vault local usando el runtime Node de SistemaMultiagente_SDLC. Usar cuando el usuario invoque /save o pida guardar continuidad."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Save
|
|
7
|
+
|
|
8
|
+
Comando canónico:
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
npx --no-install sdlc save --target . --event manual --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Reglas:
|
|
15
|
+
|
|
16
|
+
- Escribe checkpoint local en el vault.
|
|
17
|
+
- No promueve a GitHub Issue, OpenSpec ni PR sin gate humano.
|
|
18
|
+
- Marcar cualquier decision durable como pendiente de promocion al repo.
|
package/templates/manifest.yaml
CHANGED
|
@@ -15,6 +15,8 @@ templates:
|
|
|
15
15
|
target: indice-operativo.md
|
|
16
16
|
- source: .sdlc/README.md
|
|
17
17
|
target: .sdlc/README.md
|
|
18
|
+
- source: .sdlc/session.json
|
|
19
|
+
target: .sdlc/session.json
|
|
18
20
|
- source: .github/AGENTS.md
|
|
19
21
|
target: .github/AGENTS.md
|
|
20
22
|
- source: .github/agents/ownership-matrix.md
|
|
@@ -87,6 +89,8 @@ templates:
|
|
|
87
89
|
target: .graphifyignore
|
|
88
90
|
- source: scripts/continua.ps1
|
|
89
91
|
target: scripts/continua.ps1
|
|
92
|
+
- source: scripts/continua.mjs
|
|
93
|
+
target: scripts/continua.mjs
|
|
90
94
|
- source: scripts/publish-trace.ps1
|
|
91
95
|
target: scripts/publish-trace.ps1
|
|
92
96
|
- source: scripts/export-graphify-obsidian.py
|
|
@@ -178,6 +182,30 @@ templates:
|
|
|
178
182
|
target: .claude/skills/commit/SKILL.md
|
|
179
183
|
- source: .github/skills/commit/SKILL.md
|
|
180
184
|
target: .github/skills/commit/SKILL.md
|
|
185
|
+
- source: .github/skills/resume/SKILL.md
|
|
186
|
+
target: .github/skills/resume/SKILL.md
|
|
187
|
+
- source: .github/skills/save/SKILL.md
|
|
188
|
+
target: .github/skills/save/SKILL.md
|
|
189
|
+
- source: .github/skills/continua/SKILL.md
|
|
190
|
+
target: .github/skills/continua/SKILL.md
|
|
191
|
+
- source: .claude/skills/resume/SKILL.md
|
|
192
|
+
target: .claude/skills/resume/SKILL.md
|
|
193
|
+
- source: .claude/skills/save/SKILL.md
|
|
194
|
+
target: .claude/skills/save/SKILL.md
|
|
195
|
+
- source: .claude/skills/continua/SKILL.md
|
|
196
|
+
target: .claude/skills/continua/SKILL.md
|
|
197
|
+
- source: .agents/skills/resume/SKILL.md
|
|
198
|
+
target: .agents/skills/resume/SKILL.md
|
|
199
|
+
- source: .agents/skills/save/SKILL.md
|
|
200
|
+
target: .agents/skills/save/SKILL.md
|
|
201
|
+
- source: .agents/skills/continua/SKILL.md
|
|
202
|
+
target: .agents/skills/continua/SKILL.md
|
|
203
|
+
- source: .windsurf/skills/resume/SKILL.md
|
|
204
|
+
target: .windsurf/skills/resume/SKILL.md
|
|
205
|
+
- source: .windsurf/skills/save/SKILL.md
|
|
206
|
+
target: .windsurf/skills/save/SKILL.md
|
|
207
|
+
- source: .windsurf/skills/continua/SKILL.md
|
|
208
|
+
target: .windsurf/skills/continua/SKILL.md
|
|
181
209
|
- source: .github/skills/openspec-propose/SKILL.md
|
|
182
210
|
target: .github/skills/openspec-propose/SKILL.md
|
|
183
211
|
- source: .github/skills/openspec-explore/SKILL.md
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const options = { platform: "codex", ttlHours: 4, noLock: false, json: false, vaultPath: "" };
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const token = argv[i];
|
|
10
|
+
const next = argv[i + 1];
|
|
11
|
+
if (token === "--json" || token === "-Json") options.json = true;
|
|
12
|
+
else if (token === "--no-lock" || token === "-NoLock") options.noLock = true;
|
|
13
|
+
else if ((token === "--platform" || token === "-Platform") && next) { options.platform = next; i += 1; }
|
|
14
|
+
else if ((token === "--ttl-hours" || token === "-TtlHours") && next) { options.ttlHours = Number(next); i += 1; }
|
|
15
|
+
else if ((token === "--vault-path" || token === "-VaultPath") && next) { options.vaultPath = next; i += 1; }
|
|
16
|
+
}
|
|
17
|
+
return options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readText(filePath) {
|
|
21
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function preview(text, max = 700) {
|
|
25
|
+
if (!text) return null;
|
|
26
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
27
|
+
return clean.length > max ? `${clean.slice(0, max).trim()}...` : clean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function latestFile(root, extension = ".md") {
|
|
31
|
+
if (!root || !fs.existsSync(root)) return null;
|
|
32
|
+
const files = [];
|
|
33
|
+
const walk = (current) => {
|
|
34
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
35
|
+
const absolute = path.join(current, entry.name);
|
|
36
|
+
if (entry.isDirectory()) walk(absolute);
|
|
37
|
+
else if (entry.name.endsWith(extension) && entry.name !== "TEMPLATE.md") files.push(absolute);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
walk(root);
|
|
41
|
+
return files.map((file) => ({ file, mtime: fs.statSync(file).mtimeMs })).sort((a, b) => b.mtime - a.mtime)[0]?.file ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveRepo() {
|
|
45
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveVault(repo, provided) {
|
|
49
|
+
if (provided) return provided;
|
|
50
|
+
const configPath = path.join(repo, "scripts", "obsidian-memory.config.local.json");
|
|
51
|
+
if (!fs.existsSync(configPath)) return "";
|
|
52
|
+
try {
|
|
53
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
54
|
+
return config.vaultRoot || "";
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const options = parseArgs(process.argv.slice(2));
|
|
61
|
+
const repo = resolveRepo();
|
|
62
|
+
const agentState = path.join(repo, ".github", "agent-state");
|
|
63
|
+
const platformContext = path.join(agentState, "platform-context.json");
|
|
64
|
+
const currentSlicePath = path.join(agentState, "current-slice.md");
|
|
65
|
+
const phaseStatusPath = path.join(agentState, "phase-status.yaml");
|
|
66
|
+
const activeSlicesPath = path.join(agentState, "active-slices.yaml");
|
|
67
|
+
const handoffsDir = path.join(agentState, "handoffs");
|
|
68
|
+
const graphReportPath = path.join(repo, "graphify-out", "GRAPH_REPORT.md");
|
|
69
|
+
const vaultPath = resolveVault(repo, options.vaultPath);
|
|
70
|
+
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const expires = new Date(now.getTime() + options.ttlHours * 60 * 60 * 1000);
|
|
73
|
+
const currentSlice = readText(currentSlicePath);
|
|
74
|
+
const phaseStatus = readText(phaseStatusPath);
|
|
75
|
+
const activeSlices = readText(activeSlicesPath);
|
|
76
|
+
const graphReport = readText(graphReportPath);
|
|
77
|
+
const sliceId = currentSlice?.match(/`([^`]+)`/)?.[1] ?? "unknown";
|
|
78
|
+
const slicePhase = currentSlice?.match(/\bF\d+(?:\.\d+)?\b/)?.[0] ?? "unknown";
|
|
79
|
+
const latestHandoff = latestFile(handoffsDir);
|
|
80
|
+
const latestVaultCheckpoint = latestFile(vaultPath);
|
|
81
|
+
|
|
82
|
+
const result = {
|
|
83
|
+
status: "ok",
|
|
84
|
+
project: "{{project.slug}}",
|
|
85
|
+
generated_at: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
86
|
+
platform: options.platform,
|
|
87
|
+
lock_written: !options.noLock,
|
|
88
|
+
lock: {
|
|
89
|
+
owner_platform: options.platform,
|
|
90
|
+
locked_at: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
91
|
+
expires_at: expires.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
92
|
+
ttl_hours: options.ttlHours
|
|
93
|
+
},
|
|
94
|
+
current_slice: {
|
|
95
|
+
id: sliceId,
|
|
96
|
+
phase: slicePhase,
|
|
97
|
+
preview: preview(currentSlice)
|
|
98
|
+
},
|
|
99
|
+
active_slices_preview: preview(activeSlices),
|
|
100
|
+
phase_status_preview: preview(phaseStatus),
|
|
101
|
+
latest_handoff: latestHandoff,
|
|
102
|
+
graph_report: {
|
|
103
|
+
path: graphReportPath,
|
|
104
|
+
exists: Boolean(graphReport),
|
|
105
|
+
preview: preview(graphReport)
|
|
106
|
+
},
|
|
107
|
+
vault_checkpoint: latestVaultCheckpoint
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (!options.noLock) {
|
|
111
|
+
fs.mkdirSync(agentState, { recursive: true });
|
|
112
|
+
fs.writeFileSync(platformContext, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.json) {
|
|
116
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
117
|
+
} else {
|
|
118
|
+
process.stdout.write(`CONTINUA - SDLC context resume\nProject: {{project.slug}}\nPlatform: ${options.platform}\nSlice: ${sliceId} (${slicePhase})\nLatest handoff: ${latestHandoff ?? ""}\nVault checkpoint: ${latestVaultCheckpoint ?? ""}\nGraph report: ${Boolean(graphReport)}\n`);
|
|
119
|
+
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.DESCRIPTION
|
|
6
|
-
Reads .github/agent-state, graphify-out and the optional memory vault so an
|
|
7
|
-
agent can resume the active slice without rebuilding context from raw code.
|
|
8
|
-
By default it writes .github/agent-state/platform-context.json with a TTL lock.
|
|
9
|
-
Use -NoLock for a read-only preview.
|
|
3
|
+
Thin Windows wrapper for the Node-first continua runtime.
|
|
10
4
|
#>
|
|
11
5
|
|
|
12
6
|
[CmdletBinding()]
|
|
@@ -22,120 +16,16 @@ param(
|
|
|
22
16
|
Set-StrictMode -Version Latest
|
|
23
17
|
$ErrorActionPreference = 'Stop'
|
|
24
18
|
|
|
25
|
-
$
|
|
26
|
-
$
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
$PhaseStatusPath = Join-Path $AgentState 'phase-status.yaml'
|
|
30
|
-
$ActiveSlicesPath = Join-Path $AgentState 'active-slices.yaml'
|
|
31
|
-
$HandoffsDir = Join-Path $AgentState 'handoffs'
|
|
32
|
-
$GraphReportPath = Join-Path $Repo 'graphify-out\GRAPH_REPORT.md'
|
|
33
|
-
|
|
34
|
-
function Read-TextOrNull {
|
|
35
|
-
param([string] $Path)
|
|
36
|
-
if (Test-Path -LiteralPath $Path) {
|
|
37
|
-
return (Get-Content -LiteralPath $Path -Raw)
|
|
38
|
-
}
|
|
39
|
-
return $null
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function Get-Preview {
|
|
43
|
-
param([string] $Text, [int] $Max = 700)
|
|
44
|
-
if (-not $Text) { return $null }
|
|
45
|
-
if ($Text.Length -le $Max) { return $Text.Trim() }
|
|
46
|
-
return $Text.Substring(0, $Max).Trim()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function Get-LatestFile {
|
|
50
|
-
param([string] $Root, [string] $Filter = '*.md')
|
|
51
|
-
if (-not (Test-Path -LiteralPath $Root)) { return $null }
|
|
52
|
-
$files = @(Get-ChildItem -LiteralPath $Root -Filter $Filter -File -Recurse |
|
|
53
|
-
Where-Object { $_.Name -ne 'TEMPLATE.md' } |
|
|
54
|
-
Sort-Object LastWriteTimeUtc -Descending)
|
|
55
|
-
if ($files.Count -eq 0) { return $null }
|
|
56
|
-
return $files[0]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (-not $VaultPath) {
|
|
60
|
-
$configPath = Join-Path $Repo 'scripts\obsidian-memory.config.local.json'
|
|
61
|
-
if (Test-Path -LiteralPath $configPath) {
|
|
62
|
-
try {
|
|
63
|
-
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
|
64
|
-
if ($config.vaultRoot) { $VaultPath = [string] $config.vaultRoot }
|
|
65
|
-
} catch {
|
|
66
|
-
Write-Warning "No se pudo leer ${configPath}: $_"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
$now = [datetime]::UtcNow
|
|
72
|
-
$expires = $now.AddHours($TtlHours)
|
|
73
|
-
$currentSlice = Read-TextOrNull -Path $CurrentSlicePath
|
|
74
|
-
$phaseStatus = Read-TextOrNull -Path $PhaseStatusPath
|
|
75
|
-
$activeSlices = Read-TextOrNull -Path $ActiveSlicesPath
|
|
76
|
-
$graphReport = Read-TextOrNull -Path $GraphReportPath
|
|
77
|
-
$latestHandoff = Get-LatestFile -Root $HandoffsDir
|
|
78
|
-
$latestVaultCheckpoint = if ($VaultPath) { Get-LatestFile -Root $VaultPath } else { $null }
|
|
79
|
-
|
|
80
|
-
$sliceId = 'unknown'
|
|
81
|
-
$slicePhase = 'unknown'
|
|
82
|
-
if ($currentSlice -and $currentSlice -match '`([^`]+)`') {
|
|
83
|
-
$sliceId = $Matches[1]
|
|
84
|
-
}
|
|
85
|
-
if ($currentSlice -and $currentSlice -match '(F\d+(?:\.\d+)?)') {
|
|
86
|
-
$slicePhase = $Matches[1]
|
|
19
|
+
$ScriptRoot = $PSScriptRoot
|
|
20
|
+
$NodeScript = Join-Path $ScriptRoot 'continua.mjs'
|
|
21
|
+
if (-not (Test-Path -LiteralPath $NodeScript)) {
|
|
22
|
+
throw "continua.mjs not found: $NodeScript"
|
|
87
23
|
}
|
|
88
24
|
|
|
89
|
-
$
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
platform = $Platform
|
|
94
|
-
lock_written = (-not $NoLock)
|
|
95
|
-
lock = [ordered]@{
|
|
96
|
-
owner_platform = $Platform
|
|
97
|
-
locked_at = $now.ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
98
|
-
expires_at = $expires.ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
99
|
-
ttl_hours = $TtlHours
|
|
100
|
-
}
|
|
101
|
-
current_slice = [ordered]@{
|
|
102
|
-
id = $sliceId
|
|
103
|
-
phase = $slicePhase
|
|
104
|
-
preview = Get-Preview -Text $currentSlice
|
|
105
|
-
}
|
|
106
|
-
active_slices_preview = Get-Preview -Text $activeSlices
|
|
107
|
-
phase_status_preview = Get-Preview -Text $phaseStatus
|
|
108
|
-
latest_handoff = if ($latestHandoff) { $latestHandoff.FullName } else { $null }
|
|
109
|
-
graph_report = [ordered]@{
|
|
110
|
-
path = $GraphReportPath
|
|
111
|
-
exists = [bool] $graphReport
|
|
112
|
-
preview = Get-Preview -Text $graphReport
|
|
113
|
-
}
|
|
114
|
-
vault_checkpoint = if ($latestVaultCheckpoint) { $latestVaultCheckpoint.FullName } else { $null }
|
|
115
|
-
}
|
|
25
|
+
$argsList = @($NodeScript, '--platform', $Platform, '--ttl-hours', [string]$TtlHours)
|
|
26
|
+
if ($NoLock) { $argsList += '--no-lock' }
|
|
27
|
+
if ($Json) { $argsList += '--json' }
|
|
28
|
+
if ($VaultPath) { $argsList += @('--vault-path', $VaultPath) }
|
|
116
29
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
New-Item -ItemType Directory -Path $AgentState -Force | Out-Null
|
|
120
|
-
}
|
|
121
|
-
$result | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $PlatformContext -Encoding UTF8
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if ($Json) {
|
|
125
|
-
$result | ConvertTo-Json -Depth 8
|
|
126
|
-
exit 0
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
Write-Host ''
|
|
130
|
-
Write-Host 'CONTINUA - SDLC context resume'
|
|
131
|
-
Write-Host "Project: {{project.slug}}"
|
|
132
|
-
Write-Host "Platform: $Platform"
|
|
133
|
-
Write-Host "Slice: $sliceId ($slicePhase)"
|
|
134
|
-
Write-Host "Latest handoff: $($result.latest_handoff)"
|
|
135
|
-
Write-Host "Vault checkpoint: $($result.vault_checkpoint)"
|
|
136
|
-
Write-Host "Graph report: $($result.graph_report.exists)"
|
|
137
|
-
if ($NoLock) {
|
|
138
|
-
Write-Host 'Lock: preview only'
|
|
139
|
-
} else {
|
|
140
|
-
Write-Host "Lock: written until $($result.lock.expires_at)"
|
|
141
|
-
}
|
|
30
|
+
& node @argsList
|
|
31
|
+
exit $LASTEXITCODE
|