@cleocode/cleo 2026.5.101 → 2026.5.102

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/cli/index.js CHANGED
@@ -1531,6 +1531,13 @@ var init_attachment_schema = __esm({
1531
1531
  }
1532
1532
  });
1533
1533
 
1534
+ // packages/contracts/src/boundary.ts
1535
+ var init_boundary = __esm({
1536
+ "packages/contracts/src/boundary.ts"() {
1537
+ "use strict";
1538
+ }
1539
+ });
1540
+
1534
1541
  // packages/contracts/src/branch-lock.ts
1535
1542
  var init_branch_lock = __esm({
1536
1543
  "packages/contracts/src/branch-lock.ts"() {
@@ -3912,6 +3919,38 @@ var init_operations_registry = __esm({
3912
3919
  }
3913
3920
  ]
3914
3921
  },
3922
+ {
3923
+ gateway: "mutate",
3924
+ domain: "tasks",
3925
+ operation: "saga.detach",
3926
+ description: "tasks.saga.detach (mutate) \u2014 remove a Saga member relation (task_relations type=groups); idempotent; appends to .cleo/audit/saga-detach.jsonl (T10118)",
3927
+ tier: 0,
3928
+ idempotent: true,
3929
+ sessionRequired: false,
3930
+ requiredParams: ["sagaId", "memberId"],
3931
+ params: [
3932
+ {
3933
+ name: "sagaId",
3934
+ type: "string",
3935
+ required: true,
3936
+ description: "Saga task ID",
3937
+ cli: { positional: true }
3938
+ },
3939
+ {
3940
+ name: "memberId",
3941
+ type: "string",
3942
+ required: true,
3943
+ description: "Member task ID to detach from the Saga",
3944
+ cli: { positional: true }
3945
+ },
3946
+ {
3947
+ name: "reason",
3948
+ type: "string",
3949
+ required: false,
3950
+ description: "Human-readable reason recorded in the audit log entry"
3951
+ }
3952
+ ]
3953
+ },
3915
3954
  {
3916
3955
  gateway: "query",
3917
3956
  domain: "tasks",
@@ -3961,6 +4000,27 @@ var init_operations_registry = __esm({
3961
4000
  }
3962
4001
  ]
3963
4002
  },
4003
+ {
4004
+ // T10117 — detach I5-violating parentId and re-attach via task_relations
4005
+ // type=groups (ADR-073 §1.2 invariant I5). Idempotent.
4006
+ gateway: "mutate",
4007
+ domain: "tasks",
4008
+ operation: "saga.repair",
4009
+ description: "tasks.saga.repair (mutate) \u2014 detach I5-violating parentId and re-attach via task_relations type=groups; idempotent",
4010
+ tier: 0,
4011
+ idempotent: true,
4012
+ sessionRequired: false,
4013
+ requiredParams: ["sagaId"],
4014
+ params: [
4015
+ {
4016
+ name: "sagaId",
4017
+ type: "string",
4018
+ required: true,
4019
+ description: "Saga task ID (must have label=saga)",
4020
+ cli: { positional: true }
4021
+ }
4022
+ ]
4023
+ },
3964
4024
  {
3965
4025
  gateway: "mutate",
3966
4026
  domain: "tasks",
@@ -11081,6 +11141,7 @@ var init_src2 = __esm({
11081
11141
  "packages/contracts/src/index.ts"() {
11082
11142
  init_acceptance_gate_schema();
11083
11143
  init_attachment_schema();
11144
+ init_boundary();
11084
11145
  init_branch_lock();
11085
11146
  init_changesets();
11086
11147
  init_cli_category();
@@ -30688,6 +30749,15 @@ var init_sticky2 = __esm({
30688
30749
  // packages/cleo/src/dispatch/domains/tasks.ts
30689
30750
  import { getLogger as getLogger15, getProjectRoot as getProjectRoot18 } from "@cleocode/core";
30690
30751
  import { createAttachmentStore as createAttachmentStore4 } from "@cleocode/core/internal";
30752
+ import {
30753
+ sagaAdd as coreSagaAdd,
30754
+ sagaCreate as coreSagaCreate,
30755
+ detachSagaMember as coreSagaDetach,
30756
+ sagaList as coreSagaList,
30757
+ sagaMembers as coreSagaMembers,
30758
+ repairSaga as coreSagaRepair,
30759
+ sagaRollup as coreSagaRollup
30760
+ } from "@cleocode/core/sagas";
30691
30761
  async function fetchTaskAttachments(projectRoot, taskId) {
30692
30762
  try {
30693
30763
  const store = createAttachmentStore4();
@@ -30709,137 +30779,42 @@ async function fetchTaskAttachments(projectRoot, taskId) {
30709
30779
  }
30710
30780
  }
30711
30781
  async function sagaCreate(params) {
30712
- const projectRoot = getProjectRoot18();
30713
30782
  const title = typeof params.title === "string" ? params.title : "";
30714
30783
  const description = typeof params.description === "string" ? params.description : void 0;
30715
30784
  const acceptance = Array.isArray(params.acceptance) ? params.acceptance : void 0;
30716
30785
  return wrapCoreResult(
30717
- await addTaskWithSessionScope(projectRoot, {
30718
- title,
30719
- description,
30720
- labels: ["saga"],
30721
- type: "epic",
30722
- acceptance
30723
- }),
30786
+ await coreSagaCreate(getProjectRoot18(), { title, description, acceptance }),
30724
30787
  "saga.create"
30725
30788
  );
30726
30789
  }
30727
30790
  async function sagaAdd(params) {
30728
- const projectRoot = getProjectRoot18();
30729
30791
  const sagaId = typeof params.sagaId === "string" ? params.sagaId : "";
30730
30792
  const epicId = typeof params.epicId === "string" ? params.epicId : "";
30731
- if (!sagaId || !epicId) {
30732
- return lafsError("E_INVALID_INPUT", "sagaId and epicId are required", "saga.add");
30733
- }
30734
- const sagaResult = await taskShow(projectRoot, sagaId);
30735
- if (!sagaResult.success || !sagaResult.data) {
30736
- return lafsError("E_NOT_FOUND", `Saga not found: ${sagaId}`, "saga.add");
30737
- }
30738
- const sagaTask = sagaResult.data.task;
30739
- const sagaLabels = sagaTask?.labels ?? [];
30740
- if (!sagaLabels.includes("saga")) {
30741
- return lafsError("E_INVALID_INPUT", `Task ${sagaId} does not have label='saga'`, "saga.add");
30742
- }
30743
- const epicResult = await taskShow(projectRoot, epicId);
30744
- if (!epicResult.success || !epicResult.data) {
30745
- return lafsError("E_NOT_FOUND", `Epic not found: ${epicId}`, "saga.add");
30746
- }
30747
- const epicType = epicResult.data.task?.type;
30748
- if (epicType !== "epic") {
30749
- return lafsError(
30750
- "E_INVALID_INPUT",
30751
- `Task ${epicId} has type='${String(epicType)}', expected type='epic'`,
30752
- "saga.add"
30753
- );
30754
- }
30755
- const relResult = await taskRelatesAdd(projectRoot, sagaId, epicId, "groups", void 0);
30756
- if (!relResult.success) {
30757
- return lafsError(
30758
- "E_GENERAL",
30759
- relResult.error?.message ?? "Failed to link Epic to Saga",
30760
- "saga.add"
30761
- );
30762
- }
30763
- return lafsSuccess({ sagaId, epicId, added: relResult.data?.added ?? true }, "saga.add");
30793
+ return wrapCoreResult(await coreSagaAdd(getProjectRoot18(), { sagaId, epicId }), "saga.add");
30794
+ }
30795
+ async function sagaDetach(params) {
30796
+ const sagaId = typeof params.sagaId === "string" ? params.sagaId : "";
30797
+ const memberId = typeof params.memberId === "string" ? params.memberId : "";
30798
+ const reason = typeof params.reason === "string" ? params.reason : void 0;
30799
+ return wrapCoreResult(
30800
+ await coreSagaDetach(getProjectRoot18(), { sagaId, memberId, reason }),
30801
+ "saga.detach"
30802
+ );
30764
30803
  }
30765
30804
  async function sagaList() {
30766
- const projectRoot = getProjectRoot18();
30767
- const result = await taskList(projectRoot, { type: "epic", label: "saga" });
30768
- if (!result.success) {
30769
- return lafsError("E_GENERAL", result.error?.message ?? "Failed to list Sagas", "saga.list");
30770
- }
30771
- const tasks = result.data?.tasks ?? [];
30772
- const topLevel = tasks.filter((t) => {
30773
- const parentId = t.parentId;
30774
- return !parentId;
30775
- });
30776
- return lafsSuccess({ sagas: topLevel, total: topLevel.length }, "saga.list");
30805
+ return wrapCoreResult(await coreSagaList(getProjectRoot18()), "saga.list");
30777
30806
  }
30778
30807
  async function sagaMembers(params) {
30779
- const projectRoot = getProjectRoot18();
30780
30808
  const sagaId = typeof params.sagaId === "string" ? params.sagaId : "";
30781
- if (!sagaId) {
30782
- return lafsError("E_INVALID_INPUT", "sagaId is required", "saga.members");
30783
- }
30784
- const result = await taskRelates(projectRoot, sagaId);
30785
- if (!result.success) {
30786
- return lafsError(
30787
- "E_GENERAL",
30788
- result.error?.message ?? "Failed to list Saga members",
30789
- "saga.members"
30790
- );
30791
- }
30792
- const relations = result.data?.relations ?? [];
30793
- const members = relations.filter((r) => r.type === "groups");
30794
- return lafsSuccess(
30795
- {
30796
- sagaId,
30797
- members: members.map((r) => ({ epicId: r.taskId, type: r.type, reason: r.reason })),
30798
- total: members.length
30799
- },
30800
- "saga.members"
30801
- );
30809
+ return wrapCoreResult(await coreSagaMembers(getProjectRoot18(), { sagaId }), "saga.members");
30802
30810
  }
30803
30811
  async function sagaRollup(params) {
30804
- const projectRoot = getProjectRoot18();
30805
30812
  const sagaId = typeof params.sagaId === "string" ? params.sagaId : "";
30806
- if (!sagaId) {
30807
- return lafsError("E_INVALID_INPUT", "sagaId is required", "saga.rollup");
30808
- }
30809
- const relResult = await taskRelates(projectRoot, sagaId);
30810
- if (!relResult.success) {
30811
- return lafsError(
30812
- "E_GENERAL",
30813
- relResult.error?.message ?? "Failed to fetch Saga members for rollup",
30814
- "saga.rollup"
30815
- );
30816
- }
30817
- const members = (relResult.data?.relations ?? []).filter((r) => r.type === "groups");
30818
- const total = members.length;
30819
- if (total === 0) {
30820
- return lafsSuccess(
30821
- { sagaId, total: 0, done: 0, active: 0, blocked: 0, pending: 0, completionPct: 0 },
30822
- "saga.rollup"
30823
- );
30824
- }
30825
- const shows = await Promise.all(members.map((m) => taskShow(projectRoot, m.taskId)));
30826
- let done = 0;
30827
- let active = 0;
30828
- let blocked = 0;
30829
- let pending = 0;
30830
- for (const r of shows) {
30831
- if (!r.success) continue;
30832
- const status = r.data?.task?.status ?? "pending";
30833
- if (status === "done") done++;
30834
- else if (status === "active") active++;
30835
- else if (status === "blocked") blocked++;
30836
- else pending++;
30837
- }
30838
- const completionPct = total > 0 ? Math.round(done / total * 100) : 0;
30839
- return lafsSuccess(
30840
- { sagaId, total, done, active, blocked, pending, completionPct },
30841
- "saga.rollup"
30842
- );
30813
+ return wrapCoreResult(await coreSagaRollup(getProjectRoot18(), { sagaId }), "saga.rollup");
30814
+ }
30815
+ async function sagaRepair(params) {
30816
+ const sagaId = typeof params.sagaId === "string" ? params.sagaId : "";
30817
+ return wrapCoreResult(await coreSagaRepair(getProjectRoot18(), { sagaId }), "saga.repair");
30843
30818
  }
30844
30819
  var _tasksTypedHandler, QUERY_OPS11, MUTATE_OPS10, TasksHandler;
30845
30820
  var init_tasks3 = __esm({
@@ -31296,7 +31271,11 @@ var init_tasks3 = __esm({
31296
31271
  "unclaim",
31297
31272
  // Saga sub-domain (ADR-073)
31298
31273
  "saga.create",
31299
- "saga.add"
31274
+ "saga.add",
31275
+ // T10117 — repair an I5-violating saga (detach parentId, write groups edge).
31276
+ "saga.repair",
31277
+ // T10118 — repair verb for ADR-073 §1.2 I7 violations (detach saga member).
31278
+ "saga.detach"
31300
31279
  ]);
31301
31280
  TasksHandler = class {
31302
31281
  // -----------------------------------------------------------------------
@@ -31393,6 +31372,26 @@ var init_tasks3 = __esm({
31393
31372
  startTime
31394
31373
  );
31395
31374
  }
31375
+ if (operation === "saga.repair") {
31376
+ const envelope = await sagaRepair(params ?? {});
31377
+ return wrapResult(
31378
+ envelopeToEngineResult(envelope),
31379
+ "mutate",
31380
+ "tasks",
31381
+ operation,
31382
+ startTime
31383
+ );
31384
+ }
31385
+ if (operation === "saga.detach") {
31386
+ const envelope = await sagaDetach(params ?? {});
31387
+ return wrapResult(
31388
+ envelopeToEngineResult(envelope),
31389
+ "mutate",
31390
+ "tasks",
31391
+ operation,
31392
+ startTime
31393
+ );
31394
+ }
31396
31395
  } catch (error) {
31397
31396
  getLogger15("domain:tasks").error(
31398
31397
  { gateway: "mutate", domain: "tasks", operation, err: error },
@@ -31465,7 +31464,11 @@ var init_tasks3 = __esm({
31465
31464
  "unclaim",
31466
31465
  // Saga sub-domain (ADR-073)
31467
31466
  "saga.create",
31468
- "saga.add"
31467
+ "saga.add",
31468
+ // T10117 — repair an I5-violating saga.
31469
+ "saga.repair",
31470
+ // T10118 — repair verb for ADR-073 §1.2 I7 violations
31471
+ "saga.detach"
31469
31472
  ]
31470
31473
  };
31471
31474
  }
@@ -45844,6 +45847,20 @@ var init_doctor = __esm({
45844
45847
  type: "boolean",
45845
45848
  description: "Audit orphaned CLEO-generated temp directories and report what cleo gc --temp would remove (T9043)"
45846
45849
  },
45850
+ /**
45851
+ * T10119: audit every Saga (`type='epic'` + `label='saga'`) for
45852
+ * ADR-073 §1.2 invariant violations (I5/I7 + depth ladder) and
45853
+ * auto-close drift. Read-only; non-zero exit when any I-invariant
45854
+ * fails so the check is CI-gateable.
45855
+ *
45856
+ * @task T10119
45857
+ * @saga T10113
45858
+ * @epic T10209
45859
+ */
45860
+ "audit-sagas": {
45861
+ type: "boolean",
45862
+ description: "Audit every Saga for ADR-073 \xA71.2 invariant violations (I5/I7/depth) + auto-close drift (T10119)"
45863
+ },
45847
45864
  /**
45848
45865
  * T9983: migrate the worktree-include file location from
45849
45866
  * `<root>/.cleo/worktree-include` (legacy) to `<root>/.worktreeinclude`
@@ -46250,6 +46267,17 @@ var init_doctor = __esm({
46250
46267
  if (checkResult.details?.["orphans"] && checkResult.details["orphans"].length > 0 && (process.exitCode === void 0 || process.exitCode === 0)) {
46251
46268
  process.exitCode = 2;
46252
46269
  }
46270
+ } else if (args["audit-sagas"]) {
46271
+ progress.step(0, "Auditing Saga hierarchy for ADR-073 invariants");
46272
+ const { auditSagaHierarchy } = await import("@cleocode/core/doctor/saga-audit.js");
46273
+ const projectRoot = getProjectRoot33();
46274
+ const result = await auditSagaHierarchy(projectRoot);
46275
+ const summary = `Saga audit complete \u2014 ${result.sagas.length} saga(s) inspected, ${result.count} invariant violation(s), ${result.driftCount} drift warning(s)`;
46276
+ progress.complete(summary);
46277
+ cliOutput(result, { command: "doctor", operation: "doctor.audit-sagas" });
46278
+ if (result.count > 0 && (process.exitCode === void 0 || process.exitCode === 0)) {
46279
+ process.exitCode = 2;
46280
+ }
46253
46281
  } else {
46254
46282
  progress.step(0, "Checking CLEO directory");
46255
46283
  await dispatchFromCli(
@@ -46290,6 +46318,34 @@ var init_doctor = __esm({
46290
46318
  } catch {
46291
46319
  progress.complete("Health check complete");
46292
46320
  }
46321
+ try {
46322
+ const projectRoot = getProjectRoot33();
46323
+ const { auditSagaHierarchy } = await import("@cleocode/core/doctor/saga-audit.js");
46324
+ const sagaAudit = await auditSagaHierarchy(projectRoot);
46325
+ if (sagaAudit.sagas.length === 0) {
46326
+ humanLine("\nSaga Hierarchy: no sagas found in tasks.db");
46327
+ } else {
46328
+ const header = `
46329
+ Saga Hierarchy (${sagaAudit.sagas.length} saga(s), ${sagaAudit.count} violation(s), ${sagaAudit.driftCount} drift warning(s)):`;
46330
+ humanLine(header);
46331
+ for (const s of sagaAudit.sagas) {
46332
+ const violationSuffix = s.violations.length > 0 ? `, ${s.violations.length} violation(s)` : "";
46333
+ humanLine(
46334
+ ` ${s.sagaId} \xB7 status=${s.status} \xB7 ${s.doneCount}/${s.memberCount} members done${violationSuffix}`
46335
+ );
46336
+ for (const v of s.violations) {
46337
+ humanLine(` [${v.kind}] ${v.message}`);
46338
+ }
46339
+ }
46340
+ }
46341
+ if (sagaAudit.count > 0 && (process.exitCode === void 0 || process.exitCode === 0)) {
46342
+ process.exitCode = 2;
46343
+ }
46344
+ } catch (err) {
46345
+ const msg = err instanceof Error ? err.message : String(err);
46346
+ humanLine(`
46347
+ Saga Hierarchy: audit skipped (${msg})`);
46348
+ }
46293
46349
  }
46294
46350
  } catch (err) {
46295
46351
  progress.error("Health check failed");
@@ -59755,7 +59811,7 @@ __export(saga_exports, {
59755
59811
  sagaCommand: () => sagaCommand
59756
59812
  });
59757
59813
  import { parseAcceptanceCriteria } from "@cleocode/core";
59758
- var createCommand3, addCommand11, listCommand22, membersCommand, rollupCommand2, sagaCommand;
59814
+ var createCommand3, addCommand11, detachCommand2, listCommand22, membersCommand, repairCommand, rollupCommand2, sagaCommand;
59759
59815
  var init_saga = __esm({
59760
59816
  "packages/cleo/src/cli/commands/saga.ts"() {
59761
59817
  "use strict";
@@ -59827,6 +59883,38 @@ var init_saga = __esm({
59827
59883
  );
59828
59884
  }
59829
59885
  });
59886
+ detachCommand2 = defineCommand({
59887
+ meta: {
59888
+ name: "detach",
59889
+ description: "Remove a Saga member relation (task_relations type=groups) \u2014 idempotent, audit-logged"
59890
+ },
59891
+ args: {
59892
+ sagaId: {
59893
+ type: "positional",
59894
+ description: "Saga task ID",
59895
+ required: true
59896
+ },
59897
+ memberId: {
59898
+ type: "positional",
59899
+ description: "Member task ID to detach from the Saga",
59900
+ required: true
59901
+ },
59902
+ reason: {
59903
+ type: "string",
59904
+ description: "Human-readable reason recorded in the audit log entry",
59905
+ required: false
59906
+ }
59907
+ },
59908
+ async run({ args }) {
59909
+ await dispatchFromCli(
59910
+ "mutate",
59911
+ "tasks",
59912
+ "saga.detach",
59913
+ { sagaId: args.sagaId, memberId: args.memberId, reason: args.reason },
59914
+ { command: "saga", operation: "tasks.saga.detach" }
59915
+ );
59916
+ }
59917
+ });
59830
59918
  listCommand22 = defineCommand({
59831
59919
  meta: {
59832
59920
  name: "list",
@@ -59858,6 +59946,28 @@ var init_saga = __esm({
59858
59946
  );
59859
59947
  }
59860
59948
  });
59949
+ repairCommand = defineCommand({
59950
+ meta: {
59951
+ name: "repair",
59952
+ description: "Detach an I5-violating parentId from a Saga and re-attach via task_relations.type='groups' (ADR-073 \xA71.2). Idempotent."
59953
+ },
59954
+ args: {
59955
+ sagaId: {
59956
+ type: "positional",
59957
+ description: "Saga task ID (must have label=saga)",
59958
+ required: true
59959
+ }
59960
+ },
59961
+ async run({ args }) {
59962
+ await dispatchFromCli(
59963
+ "mutate",
59964
+ "tasks",
59965
+ "saga.repair",
59966
+ { sagaId: args.sagaId },
59967
+ { command: "saga", operation: "tasks.saga.repair" }
59968
+ );
59969
+ }
59970
+ });
59861
59971
  rollupCommand2 = defineCommand({
59862
59972
  meta: {
59863
59973
  name: "rollup",
@@ -59886,9 +59996,11 @@ var init_saga = __esm({
59886
59996
  subCommands: {
59887
59997
  create: createCommand3,
59888
59998
  add: addCommand11,
59999
+ detach: detachCommand2,
59889
60000
  list: listCommand22,
59890
60001
  members: membersCommand,
59891
- rollup: rollupCommand2
60002
+ rollup: rollupCommand2,
60003
+ repair: repairCommand
59892
60004
  },
59893
60005
  async run({ cmd, rawArgs }) {
59894
60006
  const firstArg = rawArgs?.find((a) => !a.startsWith("-"));
@@ -61464,7 +61576,7 @@ __export(sequence_exports, {
61464
61576
  sequenceCommand: () => sequenceCommand
61465
61577
  });
61466
61578
  import { getProjectRoot as getProjectRoot40 } from "@cleocode/core/internal";
61467
- var showCommand12, checkCommand6, repairCommand, sequenceCommand;
61579
+ var showCommand12, checkCommand6, repairCommand2, sequenceCommand;
61468
61580
  var init_sequence = __esm({
61469
61581
  "packages/cleo/src/cli/commands/sequence.ts"() {
61470
61582
  "use strict";
@@ -61495,7 +61607,7 @@ var init_sequence = __esm({
61495
61607
  );
61496
61608
  }
61497
61609
  });
61498
- repairCommand = defineCommand({
61610
+ repairCommand2 = defineCommand({
61499
61611
  meta: { name: "repair", description: "Reset counter to max + 1 if behind" },
61500
61612
  async run() {
61501
61613
  const { repairSequence } = await import("@cleocode/core/internal");
@@ -61519,7 +61631,7 @@ var init_sequence = __esm({
61519
61631
  subCommands: {
61520
61632
  show: showCommand12,
61521
61633
  check: checkCommand6,
61522
- repair: repairCommand
61634
+ repair: repairCommand2
61523
61635
  },
61524
61636
  async run({ cmd, rawArgs }) {
61525
61637
  const firstArg = rawArgs?.find((a) => !a.startsWith("-"));