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 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.3.0 |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sistema-multiagente-sdlc",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Framework reusable para instalar un SDLC multiagente gobernado en repos greenfield o legacy.",
5
5
  "type": "module",
6
6
  "bin": {
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.command = positionals[0] ?? "help";
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.3.0";
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,8 @@
1
+ ---
2
+ name: continua
3
+ description: "Continua el flujo SDLC con `npx --no-install sdlc continua --target . --platform codex --json`."
4
+ ---
5
+
6
+ # Continua
7
+
8
+ Usar el CLI Node `sdlc continua` como entrada canónica cross-IDE.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: resume
3
+ description: "Reconstruye contexto SDLC sin mutar archivos usando `npx --no-install sdlc resume --target . --markdown`."
4
+ ---
5
+
6
+ # Resume
7
+
8
+ Usar el CLI Node `sdlc resume` como fuente unica del comportamiento cross-IDE.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: save
3
+ description: "Guarda checkpoint SDLC portable usando `npx --no-install sdlc save --target . --event manual --json`."
4
+ ---
5
+
6
+ # Save
7
+
8
+ Usar el CLI Node `sdlc save`. No promover decisiones durables sin gate humano.
@@ -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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "status": "new",
3
+ "generatedBy": "SistemaMultiagente_SDLC",
4
+ "frameworkVersion": "{{frameworkVersion}}",
5
+ "project": "{{project.slug}}",
6
+ "updatedAt": null,
7
+ "runtime": {}
8
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: continua
3
+ description: "Continua el flujo SDLC usando el CLI Node `sdlc continua`."
4
+ ---
5
+
6
+ # Continua
7
+
8
+ Ejecutar `npx --no-install sdlc continua --target . --platform windsurf --json`.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: resume
3
+ description: "Reconstruye contexto SDLC sin mutar archivos usando el CLI Node `sdlc resume`."
4
+ ---
5
+
6
+ # Resume
7
+
8
+ Ejecutar `npx --no-install sdlc resume --target . --markdown`.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: save
3
+ description: "Guarda checkpoint SDLC portable usando el CLI Node `sdlc save`."
4
+ ---
5
+
6
+ # Save
7
+
8
+ Ejecutar `npx --no-install sdlc save --target . --event manual --json`.
@@ -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
@@ -27,6 +27,9 @@
27
27
  "openspec-verify",
28
28
  "operacion-cli-devops",
29
29
  "orquestacion-multiagente",
30
+ "resume",
31
+ "save",
32
+ "continua",
30
33
  "ui-ux-diseno"
31
34
  ],
32
35
  "externalCollections": [
@@ -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
- Rebuilds SDLC working context and optionally writes the multi-agent lock.
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
- $Repo = Split-Path $PSScriptRoot -Parent
26
- $AgentState = Join-Path $Repo '.github\agent-state'
27
- $PlatformContext = Join-Path $AgentState 'platform-context.json'
28
- $CurrentSlicePath = Join-Path $AgentState 'current-slice.md'
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
- $result = [ordered]@{
90
- status = 'ok'
91
- project = '{{project.slug}}'
92
- generated_at = $now.ToString('yyyy-MM-ddTHH:mm:ssZ')
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
- if (-not $NoLock) {
118
- if (-not (Test-Path -LiteralPath $AgentState)) {
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