@tacuchi/agent-workflow-cli 20.22.0 → 20.23.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/dist/adapters/git-cli.js +42 -0
- package/dist/adapters/git-cli.js.map +1 -1
- package/dist/application/artifacts-service.js +5 -5
- package/dist/application/artifacts-service.js.map +1 -1
- package/dist/application/checkpoint-service.js +60 -55
- package/dist/application/checkpoint-service.js.map +1 -1
- package/dist/application/checkpoint-write-service.js +61 -66
- package/dist/application/checkpoint-write-service.js.map +1 -1
- package/dist/application/decisiones-service.js +5 -5
- package/dist/application/decisiones-service.js.map +1 -1
- package/dist/application/export-service.js +250 -0
- package/dist/application/export-service.js.map +1 -0
- package/dist/application/fix-git-service.js +244 -0
- package/dist/application/fix-git-service.js.map +1 -0
- package/dist/application/history-update-service.js +35 -24
- package/dist/application/history-update-service.js.map +1 -1
- package/dist/application/lifecycle-target.js +39 -0
- package/dist/application/lifecycle-target.js.map +1 -0
- package/dist/application/parsers/spec-relation.js +80 -0
- package/dist/application/parsers/spec-relation.js.map +1 -0
- package/dist/application/paths-service.js +8 -0
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/persist-service.js +285 -0
- package/dist/application/persist-service.js.map +1 -0
- package/dist/application/release-data/artifacts.js +17 -21
- package/dist/application/release-data/artifacts.js.map +1 -1
- package/dist/application/resume-service.js +184 -0
- package/dist/application/resume-service.js.map +1 -0
- package/dist/application/semantic-operation/protocol.js +247 -0
- package/dist/application/semantic-operation/protocol.js.map +1 -0
- package/dist/application/semantic-operation/publish.js +80 -0
- package/dist/application/semantic-operation/publish.js.map +1 -0
- package/dist/application/session-binding-service.js +125 -0
- package/dist/application/session-binding-service.js.map +1 -0
- package/dist/application/session-close-service.js +60 -39
- package/dist/application/session-close-service.js.map +1 -1
- package/dist/application/session-create-service.js +45 -19
- package/dist/application/session-create-service.js.map +1 -1
- package/dist/application/session-resolver.js +180 -44
- package/dist/application/session-resolver.js.map +1 -1
- package/dist/application/session-resume-service.js +39 -9
- package/dist/application/session-resume-service.js.map +1 -1
- package/dist/application/sessions-service.js +0 -2
- package/dist/application/sessions-service.js.map +1 -1
- package/dist/application/status-service.js +24 -388
- package/dist/application/status-service.js.map +1 -1
- package/dist/application/tasks-service.js +5 -5
- package/dist/application/tasks-service.js.map +1 -1
- package/dist/application/workline-index-service.js +510 -0
- package/dist/application/workline-index-service.js.map +1 -0
- package/dist/cli/commands/checkpoint-read.js +10 -6
- package/dist/cli/commands/checkpoint-read.js.map +1 -1
- package/dist/cli/commands/checkpoint-write.js +38 -11
- package/dist/cli/commands/checkpoint-write.js.map +1 -1
- package/dist/cli/commands/export.js +117 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/fix-git.js +109 -0
- package/dist/cli/commands/fix-git.js.map +1 -0
- package/dist/cli/commands/generate-launch.js +35 -3
- package/dist/cli/commands/generate-launch.js.map +1 -1
- package/dist/cli/commands/history-update.js +1 -1
- package/dist/cli/commands/history-update.js.map +1 -1
- package/dist/cli/commands/index.js +11 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/persist.js +67 -0
- package/dist/cli/commands/persist.js.map +1 -0
- package/dist/cli/commands/resume-summary.js +14 -3
- package/dist/cli/commands/resume-summary.js.map +1 -1
- package/dist/cli/commands/resume.js +73 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/session-artifacts.js +12 -11
- package/dist/cli/commands/session-artifacts.js.map +1 -1
- package/dist/cli/commands/session-close.js +8 -7
- package/dist/cli/commands/session-close.js.map +1 -1
- package/dist/cli/commands/session-create.js +5 -1
- package/dist/cli/commands/session-create.js.map +1 -1
- package/dist/cli/commands/session-resume.js +16 -10
- package/dist/cli/commands/session-resume.js.map +1 -1
- package/dist/cli/commands/status.js +66 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/workspace-init.js +21 -2
- package/dist/cli/commands/workspace-init.js.map +1 -1
- package/dist/cli/context-id.js +133 -0
- package/dist/cli/context-id.js.map +1 -0
- package/dist/cli/help-groups.js +16 -1
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +44 -16
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/output-mode.js +41 -0
- package/dist/cli/output-mode.js.map +1 -0
- package/dist/cli/parser.js +6 -0
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/render.js +53 -0
- package/dist/cli/render.js.map +1 -1
- package/package.json +1 -1
- package/skills/w/commands/export-diagrams.md +19 -10
- package/skills/w/commands/export-manuals.md +20 -10
- package/skills/w/commands/export-reports.md +20 -11
- package/skills/w/commands/export-scripts.md +20 -10
- package/skills/w/commands/fix-git.md +31 -12
- package/skills/w/commands/generate-launch.md +1 -1
- package/skills/w/commands/persist.md +33 -68
- package/skills/w/commands/resume.md +25 -46
- package/skills/w/commands/status.md +12 -48
- package/skills/w/commands/workspace-init.md +3 -3
- package/skills/w/hooks/README.md +21 -2
- package/skills/w/hooks/hooks.template.json +2 -2
- package/skills/w/roles/git/ROLE.md +7 -8
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Durable conversation→session association.
|
|
4
|
+
*
|
|
5
|
+
* The registry lives at `.workflow/sessions/.bindings.json` — machine-local
|
|
6
|
+
* state, gitignored along with the sessions dir. Keys are the SHA-256 of the
|
|
7
|
+
* host's opaque conversation id (the raw id is NEVER persisted); values are
|
|
8
|
+
* canonical session folder names.
|
|
9
|
+
*
|
|
10
|
+
* Every function here is **lock-free**: the caller owns the workspace lock so
|
|
11
|
+
* one operation = one lock boundary (no nested acquisition). Reads are safe
|
|
12
|
+
* without the lock because `writeText` renames atomically onto the path.
|
|
13
|
+
*
|
|
14
|
+
* Validation is manual (DEC-001: no Zod) and **fail-closed**: an unreadable,
|
|
15
|
+
* malformed or unknown-version registry is reported as invalid and never
|
|
16
|
+
* overwritten — repairing it automatically would silently drop associations.
|
|
17
|
+
*/
|
|
18
|
+
/** Only this schema version is accepted. */
|
|
19
|
+
export const BINDINGS_VERSION = 1;
|
|
20
|
+
/** SHA-256 of the opaque conversation id — the only form that reaches disk. */
|
|
21
|
+
export function hashContextId(contextId) {
|
|
22
|
+
return createHash("sha256").update(contextId, "utf8").digest("hex");
|
|
23
|
+
}
|
|
24
|
+
function parseBindingRegistry(raw) {
|
|
25
|
+
if (raw.trim().length === 0)
|
|
26
|
+
return { ok: true, registry: emptyRegistry() };
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, reason: "el registro de bindings no es JSON válido" };
|
|
33
|
+
}
|
|
34
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
35
|
+
return { ok: false, reason: "el registro de bindings no es un objeto JSON" };
|
|
36
|
+
}
|
|
37
|
+
const { version, bindings } = parsed;
|
|
38
|
+
if (version !== BINDINGS_VERSION) {
|
|
39
|
+
return { ok: false, reason: `versión de registro no soportada: ${String(version)}` };
|
|
40
|
+
}
|
|
41
|
+
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) {
|
|
42
|
+
return { ok: false, reason: "el campo 'bindings' no es un mapa" };
|
|
43
|
+
}
|
|
44
|
+
const entries = {};
|
|
45
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
46
|
+
if (key.length === 0 || typeof value !== "string" || value.length === 0) {
|
|
47
|
+
return { ok: false, reason: `entrada de binding inválida en la clave '${key}'` };
|
|
48
|
+
}
|
|
49
|
+
entries[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, registry: { version: BINDINGS_VERSION, bindings: entries } };
|
|
52
|
+
}
|
|
53
|
+
export async function readBindingRegistry(fs, paths) {
|
|
54
|
+
const path = paths.cwdSessionBindingsFile();
|
|
55
|
+
if (!(await fs.exists(path)))
|
|
56
|
+
return { ok: true, registry: emptyRegistry() };
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = await fs.readText(path);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { ok: false, reason: `no se pudo leer el registro de bindings (${path})` };
|
|
63
|
+
}
|
|
64
|
+
return parseBindingRegistry(raw);
|
|
65
|
+
}
|
|
66
|
+
/** Which session this conversation is associated with, if any. */
|
|
67
|
+
export async function lookupBinding(fs, paths, contextId) {
|
|
68
|
+
const read = await readBindingRegistry(fs, paths);
|
|
69
|
+
if (!read.ok)
|
|
70
|
+
return { status: "invalid", reason: read.reason };
|
|
71
|
+
const folder = read.registry.bindings[hashContextId(contextId)];
|
|
72
|
+
return folder === undefined ? { status: "unbound" } : { status: "bound", folder };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Associate this conversation with `folder`, replacing only its own entry.
|
|
76
|
+
* `changed: false` when the association already pointed there (no write).
|
|
77
|
+
*/
|
|
78
|
+
export async function bindContextToSession(fs, paths, contextId, folder) {
|
|
79
|
+
const read = await readBindingRegistry(fs, paths);
|
|
80
|
+
if (!read.ok)
|
|
81
|
+
return { ok: false, reason: read.reason };
|
|
82
|
+
const key = hashContextId(contextId);
|
|
83
|
+
if (read.registry.bindings[key] === folder)
|
|
84
|
+
return { ok: true, changed: false };
|
|
85
|
+
const bindings = { ...read.registry.bindings, [key]: folder };
|
|
86
|
+
await writeBindingRegistry(fs, paths, bindings);
|
|
87
|
+
return { ok: true, changed: true };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Drop every association pointing at `folder` (session closed). Other
|
|
91
|
+
* conversations keep theirs — a closed session never redirects to another line.
|
|
92
|
+
*/
|
|
93
|
+
export async function invalidateBindingsTo(fs, paths, folder) {
|
|
94
|
+
const read = await readBindingRegistry(fs, paths);
|
|
95
|
+
if (!read.ok)
|
|
96
|
+
return { ok: false, reason: read.reason };
|
|
97
|
+
const kept = {};
|
|
98
|
+
let removed = 0;
|
|
99
|
+
for (const [key, value] of Object.entries(read.registry.bindings)) {
|
|
100
|
+
if (value === folder)
|
|
101
|
+
removed += 1;
|
|
102
|
+
else
|
|
103
|
+
kept[key] = value;
|
|
104
|
+
}
|
|
105
|
+
if (removed > 0)
|
|
106
|
+
await writeBindingRegistry(fs, paths, kept);
|
|
107
|
+
return { ok: true, removed };
|
|
108
|
+
}
|
|
109
|
+
function emptyRegistry() {
|
|
110
|
+
return { version: BINDINGS_VERSION, bindings: {} };
|
|
111
|
+
}
|
|
112
|
+
async function writeBindingRegistry(fs, paths, bindings) {
|
|
113
|
+
// Keys sorted so the file is byte-stable across runs regardless of insertion
|
|
114
|
+
// order (diffable, golden-friendly).
|
|
115
|
+
const sorted = {};
|
|
116
|
+
for (const key of Object.keys(bindings).sort()) {
|
|
117
|
+
const value = bindings[key];
|
|
118
|
+
if (value !== undefined)
|
|
119
|
+
sorted[key] = value;
|
|
120
|
+
}
|
|
121
|
+
const payload = { version: BINDINGS_VERSION, bindings: sorted };
|
|
122
|
+
await fs.mkdirp(paths.cwdSessionsDir());
|
|
123
|
+
await fs.writeText(paths.cwdSessionBindingsFile(), `${JSON.stringify(payload, null, 2)}\n`);
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=session-binding-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-binding-service.js","sourceRoot":"","sources":["../../src/application/session-binding-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,55 +1,76 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { historyFields, upsertHistoryRow } from "./history-update-service.js";
|
|
3
|
+
import { withCwdLock } from "./lock-service.js";
|
|
3
4
|
import { canonicalArtifactPath } from "./session-artifacts.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { invalidateBindingsTo } from "./session-binding-service.js";
|
|
6
|
+
import { CLOSED_MARKER, resolveSessionTarget, } from "./session-resolver.js";
|
|
7
|
+
export async function runSessionClose(fs, paths, input) {
|
|
8
|
+
// Closing is destructive to continuity: it always names its target. Falling
|
|
9
|
+
// back to "the sole active one" would let a conversation close a line it
|
|
10
|
+
// never selected.
|
|
6
11
|
if (!input.code)
|
|
7
12
|
return { error: "--code es obligatorio" };
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
10
|
-
return {
|
|
11
|
-
|
|
12
|
-
// CHECKPOINT is a resume safety net (no-op
|
|
13
|
-
// BACKLOG is NOT fabricated
|
|
14
|
-
// only when there is deferred
|
|
15
|
-
//
|
|
13
|
+
const resolution = await resolveSessionTarget(fs, paths, { code: input.code, allowClosed: true });
|
|
14
|
+
if (resolution.outcome !== "resolved")
|
|
15
|
+
return { sessionError: resolution };
|
|
16
|
+
const session = resolution.session;
|
|
17
|
+
// Durable artifacts survive close. CHECKPOINT is a resume safety net (no-op
|
|
18
|
+
// when the loop already wrote one). BACKLOG is NOT fabricated: the owning loop
|
|
19
|
+
// writes it only when there is deferred content; `backlog_path` still reports
|
|
20
|
+
// the canonical path.
|
|
16
21
|
const checkpointPath = canonicalArtifactPath(session.path, "checkpoint");
|
|
17
|
-
const backlogPath = canonicalArtifactPath(session.path, "backlog");
|
|
18
22
|
await ensureFile(fs, checkpointPath, "# CHECKPOINT\n");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const refs = input.refs?.trim();
|
|
24
|
+
const closure = await closeUnderLock(fs, paths, session, {
|
|
25
|
+
code: session.code ?? input.code,
|
|
26
|
+
...(refs !== undefined && refs.length > 0 ? { refs } : {}),
|
|
27
|
+
});
|
|
28
|
+
if ("error" in closure)
|
|
29
|
+
return closure;
|
|
22
30
|
const sessionClose = {
|
|
23
|
-
code: session.code ?? input.code
|
|
31
|
+
code: session.code ?? input.code,
|
|
24
32
|
folder: session.folder,
|
|
25
33
|
closed: true,
|
|
26
34
|
checkpoint_path: checkpointPath,
|
|
27
|
-
backlog_path:
|
|
35
|
+
backlog_path: canonicalArtifactPath(session.path, "backlog"),
|
|
36
|
+
...(refs !== undefined && refs.length > 0 ? { refs } : {}),
|
|
37
|
+
bindings_invalidated: closure.bindings_invalidated,
|
|
38
|
+
...(closure.history ? { history: closure.history } : {}),
|
|
39
|
+
...(closure.history_error !== undefined ? { history_error: closure.history_error } : {}),
|
|
28
40
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
return { sessionClose };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The whole shared-state mutation of a close, under ONE lock acquisition:
|
|
45
|
+
* invalidate the associations pointing here, write the `.closed` marker, upsert
|
|
46
|
+
* the HISTORY row. HISTORY goes through the lock-free primitive on purpose —
|
|
47
|
+
* its public command takes the lock itself, and nesting would deadlock.
|
|
48
|
+
*
|
|
49
|
+
* The registry goes FIRST: if it cannot be read, the close aborts having
|
|
50
|
+
* mutated nothing, rather than leaving a closed session that conversations are
|
|
51
|
+
* still associated with.
|
|
52
|
+
*/
|
|
53
|
+
async function closeUnderLock(fs, paths, session, row) {
|
|
54
|
+
const result = await withCwdLock(fs, paths, async () => {
|
|
55
|
+
const invalidated = await invalidateBindingsTo(fs, paths, session.folder);
|
|
56
|
+
if (!invalidated.ok) {
|
|
57
|
+
return { ok: false, failure: { error: invalidated.reason, code: "SESSION_BINDING_INVALID" } };
|
|
44
58
|
}
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
await fs.writeText(join(session.path, CLOSED_MARKER), "");
|
|
60
|
+
const closure = { bindings_invalidated: invalidated.removed };
|
|
61
|
+
try {
|
|
62
|
+
const history = await upsertHistoryRow(fs, paths, historyFields({ ...row, state: "closed" }, session, row.code));
|
|
63
|
+
closure.history = { action: history.action, state: history.state };
|
|
47
64
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Non-fatal, as before: the caller re-runs `aw history-update` on this.
|
|
67
|
+
closure.history_error = err instanceof Error ? err.message : String(err);
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, closure };
|
|
70
|
+
});
|
|
71
|
+
if ("error" in result)
|
|
72
|
+
return { error: result.error, code: "LOCK_BUSY" };
|
|
73
|
+
return result.ok ? result.closure : result.failure;
|
|
53
74
|
}
|
|
54
75
|
async function ensureFile(fs, path, defaultContent) {
|
|
55
76
|
if (await fs.exists(path))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-close-service.js","sourceRoot":"","sources":["../../src/application/session-close-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"session-close-service.js","sourceRoot":"","sources":["../../src/application/session-close-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { withCwdLock } from "./lock-service.js";
|
|
2
3
|
import { canonicalArtifactPath } from "./session-artifacts.js";
|
|
4
|
+
import { bindContextToSession, readBindingRegistry } from "./session-binding-service.js";
|
|
3
5
|
import { renderSessionMarkdown } from "./templates/session.js";
|
|
4
6
|
const VALID_TYPES = ["research", "refine", "exec", "quick"];
|
|
5
|
-
export async function runSessionCreate(fs,
|
|
7
|
+
export async function runSessionCreate(fs, paths, input) {
|
|
6
8
|
const validated = validateInput(input);
|
|
7
9
|
if ("error" in validated)
|
|
8
10
|
return validated;
|
|
9
11
|
const { type, name, objetivo } = validated;
|
|
10
|
-
const folderInfo = await
|
|
12
|
+
const folderInfo = await claimSessionFolder(fs, paths, name, input.contextId);
|
|
11
13
|
if ("error" in folderInfo)
|
|
12
14
|
return folderInfo;
|
|
13
15
|
const sessionPath = folderInfo.sessionPath;
|
|
@@ -53,23 +55,47 @@ function validateInput(input) {
|
|
|
53
55
|
return { error: "--objetivo es obligatorio" };
|
|
54
56
|
return { type: type, name, objetivo };
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Claim number + folder + conversation association under ONE lock: two
|
|
60
|
+
* concurrent creations must not read the same counter and race for the same
|
|
61
|
+
* `NNN`, and the new line must belong to its conversation the moment it exists.
|
|
62
|
+
*/
|
|
63
|
+
async function claimSessionFolder(fs, paths, name, contextId) {
|
|
64
|
+
const id = contextId?.trim() ?? "";
|
|
65
|
+
const result = await withCwdLock(fs, paths, async () => {
|
|
66
|
+
// Fail before creating anything when the registry cannot be read: a session
|
|
67
|
+
// that exists but could not be associated is worse than one never created.
|
|
68
|
+
if (id.length > 0) {
|
|
69
|
+
const registry = await readBindingRegistry(fs, paths);
|
|
70
|
+
if (!registry.ok) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
failure: { error: registry.reason, code: "SESSION_BINDING_INVALID" },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const sessionsDir = paths.cwdSessionsDir();
|
|
78
|
+
await fs.mkdirp(sessionsDir);
|
|
79
|
+
// The CLI owns the session number: a single global, sequential counter across
|
|
80
|
+
// ALL sessions in `.workflow/sessions/` (any type), so numbering never resets
|
|
81
|
+
// per type nor collides. Callers pass only the descriptor via `--name`; the
|
|
82
|
+
// `NNN-` prefix is assigned here. A descriptor that already carries a leading
|
|
83
|
+
// `NNN-` is normalized away first so the prefix can't double up.
|
|
84
|
+
const descriptor = name.replace(/^\d{3}-/, "");
|
|
85
|
+
const number = await nextSessionNumber(fs, sessionsDir);
|
|
86
|
+
const folder = `${number}-${descriptor}`;
|
|
87
|
+
const sessionPath = join(sessionsDir, folder);
|
|
88
|
+
if (await fs.exists(sessionPath)) {
|
|
89
|
+
return { ok: false, failure: { error: `Ya existe ${sessionPath}` } };
|
|
90
|
+
}
|
|
91
|
+
await fs.mkdirp(sessionPath);
|
|
92
|
+
if (id.length > 0)
|
|
93
|
+
await bindContextToSession(fs, paths, id, folder);
|
|
94
|
+
return { ok: true, info: { folder, number, sessionPath } };
|
|
95
|
+
});
|
|
96
|
+
if ("error" in result)
|
|
97
|
+
return { error: result.error, code: "LOCK_BUSY" };
|
|
98
|
+
return result.ok ? result.info : result.failure;
|
|
73
99
|
}
|
|
74
100
|
/**
|
|
75
101
|
* Next global session number: scan `.workflow/sessions/` for any entry whose name
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-create-service.js","sourceRoot":"","sources":["../../src/application/session-create-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"session-create-service.js","sourceRoot":"","sources":["../../src/application/session-create-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { localDateIso } from "./dates.js";
|
|
3
|
+
import { withCwdLock } from "./lock-service.js";
|
|
3
4
|
import { firstNonEmptyLine, parseMdSectionBilingual, parseMdValueBilingual } from "./markdown.js";
|
|
4
5
|
import { relpath } from "./paths.js";
|
|
5
6
|
import { findArtifact } from "./session-artifacts.js";
|
|
7
|
+
import { bindContextToSession, lookupBinding } from "./session-binding-service.js";
|
|
6
8
|
const SESSION_FOLDER_RE = /^session(\d{3})-(.+)$/;
|
|
7
9
|
/** Folder-local sentinel file marking a session as closed. */
|
|
8
10
|
export const CLOSED_MARKER = ".closed";
|
|
@@ -56,58 +58,192 @@ export async function listSessionFolders(fs, dir) {
|
|
|
56
58
|
.map((e) => ({ name: e.name, path: e.path }))
|
|
57
59
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Request shape shared by every session-scoped READ: an optional explicit
|
|
63
|
+
* identity, the conversation's own association as the fallback, a closed target
|
|
64
|
+
* accepted (reading a finished line is legitimate), and the association
|
|
65
|
+
* established so later operations in the same conversation stay on this line.
|
|
66
|
+
*
|
|
67
|
+
* `exactOptionalPropertyTypes` is on, hence the spreads instead of assigning
|
|
68
|
+
* `undefined`.
|
|
69
|
+
*/
|
|
70
|
+
export function sessionReadRequest(input) {
|
|
71
|
+
return {
|
|
72
|
+
...(input.code !== undefined ? { code: input.code } : {}),
|
|
73
|
+
...(input.contextId !== undefined ? { contextId: input.contextId } : {}),
|
|
74
|
+
allowClosed: true,
|
|
75
|
+
bind: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Numeric identity of a session folder: `044` for `044-<slug>`, `007` for the
|
|
80
|
+
* legacy `session007-<slug>`, `null` when the folder carries no numeric prefix.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately distinct from `parseSessionFolder().code`, which returns the
|
|
83
|
+
* WHOLE folder name for current-model folders — HISTORY rows and the commit
|
|
84
|
+
* advisor key off that shape, so it stays as it is.
|
|
85
|
+
*/
|
|
86
|
+
export function sessionNumericCode(folder) {
|
|
87
|
+
const m = folder.match(/^(?:session)?(\d+)-/);
|
|
88
|
+
return m?.[1] ?? null;
|
|
89
|
+
}
|
|
90
|
+
export async function resolveSessionTarget(fs, paths, request) {
|
|
91
|
+
const resolution = await walkPrecedence(fs, paths, request);
|
|
92
|
+
if (resolution.outcome !== "resolved" || request.bind !== true)
|
|
93
|
+
return resolution;
|
|
94
|
+
return (await establishBinding(fs, paths, request.contextId, resolution)) ?? resolution;
|
|
95
|
+
}
|
|
96
|
+
async function walkPrecedence(fs, paths, request) {
|
|
97
|
+
const folders = await listSessionFolders(fs, paths.cwdSessionsDir());
|
|
98
|
+
const scanned = await scanFolders(fs, folders);
|
|
99
|
+
const code = request.code?.trim() ?? "";
|
|
100
|
+
if (code.length > 0) {
|
|
101
|
+
return resolveExplicit(fs, scanned, code, request.allowClosed === true);
|
|
102
|
+
}
|
|
103
|
+
const contextId = request.contextId?.trim() ?? "";
|
|
104
|
+
if (contextId.length > 0) {
|
|
105
|
+
const viaBinding = await resolveFromBinding(fs, paths, scanned, contextId);
|
|
106
|
+
if (viaBinding !== null)
|
|
107
|
+
return viaBinding;
|
|
108
|
+
}
|
|
109
|
+
return resolveSoleActive(fs, scanned);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Persist the association a successful resolution implies. Both an explicit
|
|
113
|
+
* selection and the sole-active fallback establish it (`via: "binding"` already
|
|
114
|
+
* IS the association, and a conversation with no id has nothing to associate).
|
|
115
|
+
* Returns an error to replace the resolution when the registry cannot be
|
|
116
|
+
* written — fail-closed: a caller must never believe it got associated when it
|
|
117
|
+
* did not. `null` = nothing to report.
|
|
118
|
+
*/
|
|
119
|
+
async function establishBinding(fs, paths, contextId, resolution) {
|
|
120
|
+
const id = contextId?.trim() ?? "";
|
|
121
|
+
if (id.length === 0 || resolution.via === "binding")
|
|
66
122
|
return null;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for (const folder of folders) {
|
|
71
|
-
const { code } = parseSessionFolder(folder.name);
|
|
72
|
-
// Match the legacy numeric code, an exact new-model folder name, or a
|
|
73
|
-
// prefix up to a `-` word boundary. The boundary matters: a bare
|
|
74
|
-
// `startsWith(code)` lets "100" fuzzy-match "1000-…" (reachable once the
|
|
75
|
-
// global counter passes 999 and 4-digit prefixes coexist with 3-digit
|
|
76
|
-
// ones) or "01" match "012-…", silently resolving the wrong session.
|
|
77
|
-
// Anchor on the normalized `lookupCode` so abbreviated numeric codes
|
|
78
|
-
// (e.g. `--code 1`) resolve consistently too.
|
|
79
|
-
if (code === lookupCode ||
|
|
80
|
-
folder.name === lookupCode ||
|
|
81
|
-
folder.name.startsWith(`${lookupCode}-`)) {
|
|
82
|
-
return buildSessionEntry(fs, folder.path, folder.name);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
123
|
+
// Never associate a conversation with a closed session: the binding path
|
|
124
|
+
// treats a closed target as stale, so writing one would poison the next read.
|
|
125
|
+
if (resolution.session.state === "closed")
|
|
85
126
|
return null;
|
|
86
|
-
|
|
127
|
+
// Optimistic, and deliberately so. The common case is an association that
|
|
128
|
+
// already points here — a loop re-running `--code NNN` every turn. Reading is
|
|
129
|
+
// lock-free, so an ordinary session-scoped read neither contends for the
|
|
130
|
+
// workspace lock nor starts failing while another operation holds it.
|
|
131
|
+
const current = await lookupBinding(fs, paths, id);
|
|
132
|
+
if (current.status === "bound" && current.folder === resolution.session.folder)
|
|
133
|
+
return null;
|
|
134
|
+
if (current.status === "invalid")
|
|
135
|
+
return bindingFailure(resolution.session, current.reason);
|
|
136
|
+
const written = await withCwdLock(fs, paths, () => bindContextToSession(fs, paths, id, resolution.session.folder));
|
|
137
|
+
if ("error" in written)
|
|
138
|
+
return bindingFailure(resolution.session, written.error);
|
|
139
|
+
return written.ok ? null : bindingFailure(resolution.session, written.reason);
|
|
140
|
+
}
|
|
141
|
+
function bindingFailure(session, reason) {
|
|
142
|
+
return {
|
|
143
|
+
outcome: "error",
|
|
144
|
+
code: "SESSION_BINDING_INVALID",
|
|
145
|
+
message: `no se pudo asociar la conversación a '${session.folder}': ${reason}`,
|
|
146
|
+
candidates: [
|
|
147
|
+
{ folder: session.folder, code: sessionNumericCode(session.folder), state: session.state },
|
|
148
|
+
],
|
|
149
|
+
action: "inspeccioná o retirá `.workflow/sessions/.bindings.json` y reintentá",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function scanFolders(fs, folders) {
|
|
153
|
+
const out = [];
|
|
87
154
|
for (const folder of folders) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
155
|
+
out.push({
|
|
156
|
+
name: folder.name,
|
|
157
|
+
path: folder.path,
|
|
158
|
+
code: sessionNumericCode(folder.name),
|
|
159
|
+
state: await stateFromClosedMarker(fs, folder.path),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
async function resolveExplicit(fs, scanned, code, allowClosed) {
|
|
165
|
+
const matches = scanned.filter((s) => matchesIdentity(s.name, code));
|
|
166
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
167
|
+
if (only === undefined) {
|
|
168
|
+
return matches.length === 0
|
|
169
|
+
? resolutionError("SESSION_NOT_FOUND", `no existe una sesión que coincida con '${code}'`, scanned, "revisá `aw sessions` y reintentá con un código o carpeta existente")
|
|
170
|
+
: resolutionError("SESSION_AMBIGUOUS", `'${code}' coincide con ${matches.length} sesiones`, matches, "reintentá con el nombre exacto de la carpeta");
|
|
92
171
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
if (first) {
|
|
96
|
-
return buildSessionEntry(fs, first.path, first.name);
|
|
97
|
-
}
|
|
172
|
+
if (only.state === "closed" && !allowClosed) {
|
|
173
|
+
return resolutionError("SESSION_CLOSED", `la sesión '${only.name}' está cerrada`, [only], "reabrila con `aw session-resume --code <NNN> --reopen` o elegí una sesión activa");
|
|
98
174
|
}
|
|
99
|
-
return
|
|
175
|
+
return {
|
|
176
|
+
outcome: "resolved",
|
|
177
|
+
via: "explicit",
|
|
178
|
+
session: await buildSessionEntry(fs, only.path, only.name),
|
|
179
|
+
};
|
|
100
180
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
return
|
|
181
|
+
/** `null` = this conversation has no association; keep walking the precedence. */
|
|
182
|
+
async function resolveFromBinding(fs, paths, scanned, contextId) {
|
|
183
|
+
const lookup = await lookupBinding(fs, paths, contextId);
|
|
184
|
+
if (lookup.status === "unbound")
|
|
185
|
+
return null;
|
|
186
|
+
if (lookup.status === "invalid") {
|
|
187
|
+
return resolutionError("SESSION_BINDING_INVALID", lookup.reason, scanned, "inspeccioná o retirá `.workflow/sessions/.bindings.json`, o indicá la sesión con --code");
|
|
188
|
+
}
|
|
189
|
+
// A stale association fails closed. Redirecting it to another active session
|
|
190
|
+
// is exactly the session mixing this resolver exists to prevent.
|
|
191
|
+
const target = scanned.find((s) => s.name === lookup.folder);
|
|
192
|
+
if (target === undefined) {
|
|
193
|
+
return resolutionError("SESSION_BINDING_INVALID", `la conversación está asociada a '${lookup.folder}', que ya no existe`, scanned, "reasociá la conversación reanudando explícitamente con --code");
|
|
194
|
+
}
|
|
195
|
+
if (target.state === "closed") {
|
|
196
|
+
return resolutionError("SESSION_BINDING_INVALID", `la conversación está asociada a '${lookup.folder}', que está cerrada`, scanned, "reasociá la conversación con `aw session-resume --code <NNN> --reopen`");
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
outcome: "resolved",
|
|
200
|
+
via: "binding",
|
|
201
|
+
session: await buildSessionEntry(fs, target.path, target.name),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function resolveSoleActive(fs, scanned) {
|
|
205
|
+
const actives = scanned.filter((s) => s.state === "active");
|
|
206
|
+
const only = actives.length === 1 ? actives[0] : undefined;
|
|
207
|
+
if (only !== undefined) {
|
|
208
|
+
return {
|
|
209
|
+
outcome: "resolved",
|
|
210
|
+
via: "sole_active",
|
|
211
|
+
session: await buildSessionEntry(fs, only.path, only.name),
|
|
212
|
+
};
|
|
106
213
|
}
|
|
107
|
-
if (
|
|
108
|
-
return
|
|
214
|
+
if (actives.length === 0) {
|
|
215
|
+
return resolutionError("SESSION_NOT_FOUND", "no hay sesiones activas en el workspace", scanned, "creá una con `aw session-create` o indicá una existente con --code");
|
|
109
216
|
}
|
|
110
|
-
return
|
|
217
|
+
return resolutionError("SESSION_AMBIGUOUS", `hay ${actives.length} sesiones activas y la conversación no tiene una asociación`, actives, "indicá cuál con --code <NNN>");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Identity match, anchored on a `-` word boundary so `100` never matches
|
|
221
|
+
* `1000-…` nor `01` matches `012-…`. A numeric identity also reaches the legacy
|
|
222
|
+
* `sessionNNN-…` layout, so one code resolves a session in either format.
|
|
223
|
+
*/
|
|
224
|
+
function matchesIdentity(folder, input) {
|
|
225
|
+
if (folder === input)
|
|
226
|
+
return true;
|
|
227
|
+
const digits = numericIdentity(input);
|
|
228
|
+
if (digits !== null) {
|
|
229
|
+
return folder.startsWith(`${digits}-`) || folder.startsWith(`session${digits}-`);
|
|
230
|
+
}
|
|
231
|
+
return folder.startsWith(`${input}-`);
|
|
232
|
+
}
|
|
233
|
+
function numericIdentity(input) {
|
|
234
|
+
if (/^\d+$/.test(input))
|
|
235
|
+
return input.padStart(3, "0");
|
|
236
|
+
const legacy = input.match(/^session(\d+)/);
|
|
237
|
+
return legacy?.[1] ? legacy[1].padStart(3, "0") : null;
|
|
238
|
+
}
|
|
239
|
+
function resolutionError(code, message, candidates, action) {
|
|
240
|
+
return {
|
|
241
|
+
outcome: "error",
|
|
242
|
+
code,
|
|
243
|
+
message,
|
|
244
|
+
candidates: candidates.map((s) => ({ folder: s.name, code: s.code, state: s.state })),
|
|
245
|
+
action,
|
|
246
|
+
};
|
|
111
247
|
}
|
|
112
248
|
/**
|
|
113
249
|
* Fallback when SESSION.md carries no `## Type` (no longer rendered): derive
|