@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.
Files changed (109) hide show
  1. package/dist/adapters/git-cli.js +42 -0
  2. package/dist/adapters/git-cli.js.map +1 -1
  3. package/dist/application/artifacts-service.js +5 -5
  4. package/dist/application/artifacts-service.js.map +1 -1
  5. package/dist/application/checkpoint-service.js +60 -55
  6. package/dist/application/checkpoint-service.js.map +1 -1
  7. package/dist/application/checkpoint-write-service.js +61 -66
  8. package/dist/application/checkpoint-write-service.js.map +1 -1
  9. package/dist/application/decisiones-service.js +5 -5
  10. package/dist/application/decisiones-service.js.map +1 -1
  11. package/dist/application/export-service.js +250 -0
  12. package/dist/application/export-service.js.map +1 -0
  13. package/dist/application/fix-git-service.js +244 -0
  14. package/dist/application/fix-git-service.js.map +1 -0
  15. package/dist/application/history-update-service.js +35 -24
  16. package/dist/application/history-update-service.js.map +1 -1
  17. package/dist/application/lifecycle-target.js +39 -0
  18. package/dist/application/lifecycle-target.js.map +1 -0
  19. package/dist/application/parsers/spec-relation.js +80 -0
  20. package/dist/application/parsers/spec-relation.js.map +1 -0
  21. package/dist/application/paths-service.js +8 -0
  22. package/dist/application/paths-service.js.map +1 -1
  23. package/dist/application/persist-service.js +285 -0
  24. package/dist/application/persist-service.js.map +1 -0
  25. package/dist/application/release-data/artifacts.js +17 -21
  26. package/dist/application/release-data/artifacts.js.map +1 -1
  27. package/dist/application/resume-service.js +184 -0
  28. package/dist/application/resume-service.js.map +1 -0
  29. package/dist/application/semantic-operation/protocol.js +247 -0
  30. package/dist/application/semantic-operation/protocol.js.map +1 -0
  31. package/dist/application/semantic-operation/publish.js +80 -0
  32. package/dist/application/semantic-operation/publish.js.map +1 -0
  33. package/dist/application/session-binding-service.js +125 -0
  34. package/dist/application/session-binding-service.js.map +1 -0
  35. package/dist/application/session-close-service.js +60 -39
  36. package/dist/application/session-close-service.js.map +1 -1
  37. package/dist/application/session-create-service.js +45 -19
  38. package/dist/application/session-create-service.js.map +1 -1
  39. package/dist/application/session-resolver.js +180 -44
  40. package/dist/application/session-resolver.js.map +1 -1
  41. package/dist/application/session-resume-service.js +39 -9
  42. package/dist/application/session-resume-service.js.map +1 -1
  43. package/dist/application/sessions-service.js +0 -2
  44. package/dist/application/sessions-service.js.map +1 -1
  45. package/dist/application/status-service.js +24 -388
  46. package/dist/application/status-service.js.map +1 -1
  47. package/dist/application/tasks-service.js +5 -5
  48. package/dist/application/tasks-service.js.map +1 -1
  49. package/dist/application/workline-index-service.js +510 -0
  50. package/dist/application/workline-index-service.js.map +1 -0
  51. package/dist/cli/commands/checkpoint-read.js +10 -6
  52. package/dist/cli/commands/checkpoint-read.js.map +1 -1
  53. package/dist/cli/commands/checkpoint-write.js +38 -11
  54. package/dist/cli/commands/checkpoint-write.js.map +1 -1
  55. package/dist/cli/commands/export.js +117 -0
  56. package/dist/cli/commands/export.js.map +1 -0
  57. package/dist/cli/commands/fix-git.js +109 -0
  58. package/dist/cli/commands/fix-git.js.map +1 -0
  59. package/dist/cli/commands/generate-launch.js +35 -3
  60. package/dist/cli/commands/generate-launch.js.map +1 -1
  61. package/dist/cli/commands/history-update.js +1 -1
  62. package/dist/cli/commands/history-update.js.map +1 -1
  63. package/dist/cli/commands/index.js +11 -0
  64. package/dist/cli/commands/index.js.map +1 -1
  65. package/dist/cli/commands/persist.js +67 -0
  66. package/dist/cli/commands/persist.js.map +1 -0
  67. package/dist/cli/commands/resume-summary.js +14 -3
  68. package/dist/cli/commands/resume-summary.js.map +1 -1
  69. package/dist/cli/commands/resume.js +73 -0
  70. package/dist/cli/commands/resume.js.map +1 -0
  71. package/dist/cli/commands/session-artifacts.js +12 -11
  72. package/dist/cli/commands/session-artifacts.js.map +1 -1
  73. package/dist/cli/commands/session-close.js +8 -7
  74. package/dist/cli/commands/session-close.js.map +1 -1
  75. package/dist/cli/commands/session-create.js +5 -1
  76. package/dist/cli/commands/session-create.js.map +1 -1
  77. package/dist/cli/commands/session-resume.js +16 -10
  78. package/dist/cli/commands/session-resume.js.map +1 -1
  79. package/dist/cli/commands/status.js +66 -2
  80. package/dist/cli/commands/status.js.map +1 -1
  81. package/dist/cli/commands/workspace-init.js +21 -2
  82. package/dist/cli/commands/workspace-init.js.map +1 -1
  83. package/dist/cli/context-id.js +133 -0
  84. package/dist/cli/context-id.js.map +1 -0
  85. package/dist/cli/help-groups.js +16 -1
  86. package/dist/cli/help-groups.js.map +1 -1
  87. package/dist/cli/main.js +44 -16
  88. package/dist/cli/main.js.map +1 -1
  89. package/dist/cli/output-mode.js +41 -0
  90. package/dist/cli/output-mode.js.map +1 -0
  91. package/dist/cli/parser.js +6 -0
  92. package/dist/cli/parser.js.map +1 -1
  93. package/dist/cli/registry.js.map +1 -1
  94. package/dist/cli/render.js +53 -0
  95. package/dist/cli/render.js.map +1 -1
  96. package/package.json +1 -1
  97. package/skills/w/commands/export-diagrams.md +19 -10
  98. package/skills/w/commands/export-manuals.md +20 -10
  99. package/skills/w/commands/export-reports.md +20 -11
  100. package/skills/w/commands/export-scripts.md +20 -10
  101. package/skills/w/commands/fix-git.md +31 -12
  102. package/skills/w/commands/generate-launch.md +1 -1
  103. package/skills/w/commands/persist.md +33 -68
  104. package/skills/w/commands/resume.md +25 -46
  105. package/skills/w/commands/status.md +12 -48
  106. package/skills/w/commands/workspace-init.md +3 -3
  107. package/skills/w/hooks/README.md +21 -2
  108. package/skills/w/hooks/hooks.template.json +2 -2
  109. 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 { runHistoryUpdate } from "./history-update-service.js";
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 { CLOSED_MARKER, resolveSession } from "./session-resolver.js";
5
- export async function runSessionClose(fs, env, paths, input) {
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 session = await resolveSession(fs, env, paths, input.code, true);
9
- if (!session)
10
- return { error: `Sesión no encontrada: ${input.code}` };
11
- // Persist the durable artifacts in the session folder (they survive close).
12
- // CHECKPOINT is a resume safety net (no-op when the loop already wrote one).
13
- // BACKLOG is NOT fabricated here: the owning loop writes a real BACKLOG.md
14
- // only when there is deferred/followup content, so close no longer creates
15
- // an empty boilerplate file. `backlog_path` still reports the canonical path.
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
- // Mark the session closed via the folder-local sentinel file.
20
- // Sessions are internal/light: closing does not touch the project block.
21
- await fs.writeText(join(session.path, CLOSED_MARKER), "");
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: backlogPath,
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
- if (input.refs && input.refs.trim().length > 0) {
30
- sessionClose.refs = input.refs.trim();
31
- }
32
- // Sessions are gitignored (machine-local live log); HISTORY.md is the durable,
33
- // committable record close upserts its row here so it actually gets written
34
- // (doctrine-only wiring proved dead: nothing ever called `aw history-update`).
35
- // Non-fatal: a busy lock or a write failure never blocks closing.
36
- try {
37
- const history = await runHistoryUpdate(fs, env, paths, {
38
- code: sessionClose.code,
39
- state: "closed",
40
- ...(sessionClose.refs !== undefined ? { refs: sessionClose.refs } : {}),
41
- });
42
- if ("error" in history) {
43
- sessionClose.history_error = history.error;
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
- else {
46
- sessionClose.history = { action: history.action, state: history.state };
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
- catch (err) {
50
- sessionClose.history_error = err instanceof Error ? err.message : String(err);
51
- }
52
- return { sessionClose };
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":"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"}
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, _env, paths, input) {
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 prepareSessionFolder(fs, paths, name);
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
- async function prepareSessionFolder(fs, paths, name) {
57
- const sessionsDir = paths.cwdSessionsDir();
58
- await fs.mkdirp(sessionsDir);
59
- // The CLI owns the session number: a single global, sequential counter across
60
- // ALL sessions in `.workflow/sessions/` (any type), so numbering never resets
61
- // per type nor collides. Callers pass only the descriptor via `--name`; the
62
- // `NNN-` prefix is assigned here. A descriptor that already carries a leading
63
- // `NNN-` is normalized away first so the prefix can't double up.
64
- const descriptor = name.replace(/^\d{3}-/, "");
65
- const number = await nextSessionNumber(fs, sessionsDir);
66
- const folder = `${number}-${descriptor}`;
67
- const sessionPath = join(sessionsDir, folder);
68
- if (await fs.exists(sessionPath)) {
69
- return { error: `Ya existe ${sessionPath}` };
70
- }
71
- await fs.mkdirp(sessionPath);
72
- return { folder, number, sessionPath, sessionsDir };
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":"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"}
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
- export async function resolveSession(fs, env, paths, sessionCode, anyState = false) {
60
- // env preserved in signature for symmetry with sibling services; the
61
- // sessions directory now resolves via paths so namespace flows through.
62
- void env;
63
- const dir = paths.cwdSessionsDir();
64
- const folders = await listSessionFolders(fs, dir);
65
- if (folders.length === 0)
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
- folders.sort((a, b) => b.name.localeCompare(a.name));
68
- if (sessionCode !== undefined && sessionCode.length > 0) {
69
- const lookupCode = normalizeCode(sessionCode);
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
- const entry = await buildSessionEntry(fs, folder.path, folder.name);
89
- if (entry.state === "active") {
90
- return entry;
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 (anyState) {
94
- const first = folders[0];
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 null;
175
+ return {
176
+ outcome: "resolved",
177
+ via: "explicit",
178
+ session: await buildSessionEntry(fs, only.path, only.name),
179
+ };
100
180
  }
101
- function normalizeCode(input) {
102
- // Legacy numeric codes pad to 3 digits; legacy `sessionNNN-...` strips the prefix.
103
- // New-model slugs are used verbatim (the folder name is the identity).
104
- if (/^\d+$/.test(input)) {
105
- return input.padStart(3, "0");
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 (/^session\d/.test(input)) {
108
- return input.replace("session", "").split("-")[0] ?? "";
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 input;
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