@rpamis/comet 0.4.0-beta.5 → 0.4.0-beta.6

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 (53) hide show
  1. package/README.md +6 -3
  2. package/assets/manifest.json +1 -1
  3. package/assets/skills/comet/SKILL.md +1 -1
  4. package/assets/skills/comet/reference/comet-yaml-fields.md +4 -2
  5. package/assets/skills/comet/scripts/comet-runtime.mjs +504 -260
  6. package/assets/skills/comet-archive/SKILL.md +6 -7
  7. package/assets/skills/comet-build/SKILL.md +16 -10
  8. package/assets/skills/comet-hotfix/SKILL.md +16 -2
  9. package/assets/skills/comet-tweak/SKILL.md +15 -3
  10. package/assets/skills/comet-verify/SKILL.md +2 -0
  11. package/assets/skills-zh/comet/SKILL.md +1 -1
  12. package/assets/skills-zh/comet/reference/comet-yaml-fields.md +4 -2
  13. package/assets/skills-zh/comet-archive/SKILL.md +6 -7
  14. package/assets/skills-zh/comet-build/SKILL.md +16 -10
  15. package/assets/skills-zh/comet-hotfix/SKILL.md +16 -2
  16. package/assets/skills-zh/comet-tweak/SKILL.md +15 -3
  17. package/assets/skills-zh/comet-verify/SKILL.md +2 -0
  18. package/dist/app/commands/status.d.ts +1 -0
  19. package/dist/app/commands/status.d.ts.map +1 -1
  20. package/dist/app/commands/status.js +10 -0
  21. package/dist/app/commands/status.js.map +1 -1
  22. package/dist/domains/comet-classic/classic-branch-binding.d.ts +51 -0
  23. package/dist/domains/comet-classic/classic-branch-binding.d.ts.map +1 -0
  24. package/dist/domains/comet-classic/classic-branch-binding.js +107 -0
  25. package/dist/domains/comet-classic/classic-branch-binding.js.map +1 -0
  26. package/dist/domains/comet-classic/classic-current-change.d.ts.map +1 -1
  27. package/dist/domains/comet-classic/classic-current-change.js +37 -20
  28. package/dist/domains/comet-classic/classic-current-change.js.map +1 -1
  29. package/dist/domains/comet-classic/classic-guard.d.ts.map +1 -1
  30. package/dist/domains/comet-classic/classic-guard.js +40 -10
  31. package/dist/domains/comet-classic/classic-guard.js.map +1 -1
  32. package/dist/domains/comet-classic/classic-hook-guard.d.ts.map +1 -1
  33. package/dist/domains/comet-classic/classic-hook-guard.js +25 -2
  34. package/dist/domains/comet-classic/classic-hook-guard.js.map +1 -1
  35. package/dist/domains/comet-classic/classic-state-command.d.ts.map +1 -1
  36. package/dist/domains/comet-classic/classic-state-command.js +94 -4
  37. package/dist/domains/comet-classic/classic-state-command.js.map +1 -1
  38. package/dist/domains/comet-classic/classic-state-events.d.ts +1 -1
  39. package/dist/domains/comet-classic/classic-state-events.d.ts.map +1 -1
  40. package/dist/domains/comet-classic/classic-state.d.ts +2 -1
  41. package/dist/domains/comet-classic/classic-state.d.ts.map +1 -1
  42. package/dist/domains/comet-classic/classic-state.js +3 -0
  43. package/dist/domains/comet-classic/classic-state.js.map +1 -1
  44. package/dist/domains/comet-classic/classic-transitions.d.ts.map +1 -1
  45. package/dist/domains/comet-classic/classic-transitions.js +1 -0
  46. package/dist/domains/comet-classic/classic-transitions.js.map +1 -1
  47. package/dist/domains/comet-classic/classic-validate-command.d.ts.map +1 -1
  48. package/dist/domains/comet-classic/classic-validate-command.js +6 -0
  49. package/dist/domains/comet-classic/classic-validate-command.js.map +1 -1
  50. package/dist/domains/skill/find.d.ts.map +1 -1
  51. package/dist/domains/skill/find.js +7 -1
  52. package/dist/domains/skill/find.js.map +1 -1
  53. package/package.json +1 -1
@@ -126,17 +126,17 @@ var require_visit = __commonJS({
126
126
  visit.BREAK = BREAK;
127
127
  visit.SKIP = SKIP;
128
128
  visit.REMOVE = REMOVE;
129
- function visit_(key, node, visitor, path22) {
130
- const ctrl = callVisitor(key, node, visitor, path22);
129
+ function visit_(key, node, visitor, path23) {
130
+ const ctrl = callVisitor(key, node, visitor, path23);
131
131
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
132
- replaceNode(key, path22, ctrl);
133
- return visit_(key, ctrl, visitor, path22);
132
+ replaceNode(key, path23, ctrl);
133
+ return visit_(key, ctrl, visitor, path23);
134
134
  }
135
135
  if (typeof ctrl !== "symbol") {
136
136
  if (identity.isCollection(node)) {
137
- path22 = Object.freeze(path22.concat(node));
137
+ path23 = Object.freeze(path23.concat(node));
138
138
  for (let i = 0; i < node.items.length; ++i) {
139
- const ci = visit_(i, node.items[i], visitor, path22);
139
+ const ci = visit_(i, node.items[i], visitor, path23);
140
140
  if (typeof ci === "number")
141
141
  i = ci - 1;
142
142
  else if (ci === BREAK)
@@ -147,13 +147,13 @@ var require_visit = __commonJS({
147
147
  }
148
148
  }
149
149
  } else if (identity.isPair(node)) {
150
- path22 = Object.freeze(path22.concat(node));
151
- const ck = visit_("key", node.key, visitor, path22);
150
+ path23 = Object.freeze(path23.concat(node));
151
+ const ck = visit_("key", node.key, visitor, path23);
152
152
  if (ck === BREAK)
153
153
  return BREAK;
154
154
  else if (ck === REMOVE)
155
155
  node.key = null;
156
- const cv = visit_("value", node.value, visitor, path22);
156
+ const cv = visit_("value", node.value, visitor, path23);
157
157
  if (cv === BREAK)
158
158
  return BREAK;
159
159
  else if (cv === REMOVE)
@@ -174,17 +174,17 @@ var require_visit = __commonJS({
174
174
  visitAsync.BREAK = BREAK;
175
175
  visitAsync.SKIP = SKIP;
176
176
  visitAsync.REMOVE = REMOVE;
177
- async function visitAsync_(key, node, visitor, path22) {
178
- const ctrl = await callVisitor(key, node, visitor, path22);
177
+ async function visitAsync_(key, node, visitor, path23) {
178
+ const ctrl = await callVisitor(key, node, visitor, path23);
179
179
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
180
- replaceNode(key, path22, ctrl);
181
- return visitAsync_(key, ctrl, visitor, path22);
180
+ replaceNode(key, path23, ctrl);
181
+ return visitAsync_(key, ctrl, visitor, path23);
182
182
  }
183
183
  if (typeof ctrl !== "symbol") {
184
184
  if (identity.isCollection(node)) {
185
- path22 = Object.freeze(path22.concat(node));
185
+ path23 = Object.freeze(path23.concat(node));
186
186
  for (let i = 0; i < node.items.length; ++i) {
187
- const ci = await visitAsync_(i, node.items[i], visitor, path22);
187
+ const ci = await visitAsync_(i, node.items[i], visitor, path23);
188
188
  if (typeof ci === "number")
189
189
  i = ci - 1;
190
190
  else if (ci === BREAK)
@@ -195,13 +195,13 @@ var require_visit = __commonJS({
195
195
  }
196
196
  }
197
197
  } else if (identity.isPair(node)) {
198
- path22 = Object.freeze(path22.concat(node));
199
- const ck = await visitAsync_("key", node.key, visitor, path22);
198
+ path23 = Object.freeze(path23.concat(node));
199
+ const ck = await visitAsync_("key", node.key, visitor, path23);
200
200
  if (ck === BREAK)
201
201
  return BREAK;
202
202
  else if (ck === REMOVE)
203
203
  node.key = null;
204
- const cv = await visitAsync_("value", node.value, visitor, path22);
204
+ const cv = await visitAsync_("value", node.value, visitor, path23);
205
205
  if (cv === BREAK)
206
206
  return BREAK;
207
207
  else if (cv === REMOVE)
@@ -228,23 +228,23 @@ var require_visit = __commonJS({
228
228
  }
229
229
  return visitor;
230
230
  }
231
- function callVisitor(key, node, visitor, path22) {
231
+ function callVisitor(key, node, visitor, path23) {
232
232
  if (typeof visitor === "function")
233
- return visitor(key, node, path22);
233
+ return visitor(key, node, path23);
234
234
  if (identity.isMap(node))
235
- return visitor.Map?.(key, node, path22);
235
+ return visitor.Map?.(key, node, path23);
236
236
  if (identity.isSeq(node))
237
- return visitor.Seq?.(key, node, path22);
237
+ return visitor.Seq?.(key, node, path23);
238
238
  if (identity.isPair(node))
239
- return visitor.Pair?.(key, node, path22);
239
+ return visitor.Pair?.(key, node, path23);
240
240
  if (identity.isScalar(node))
241
- return visitor.Scalar?.(key, node, path22);
241
+ return visitor.Scalar?.(key, node, path23);
242
242
  if (identity.isAlias(node))
243
- return visitor.Alias?.(key, node, path22);
243
+ return visitor.Alias?.(key, node, path23);
244
244
  return void 0;
245
245
  }
246
- function replaceNode(key, path22, node) {
247
- const parent = path22[path22.length - 1];
246
+ function replaceNode(key, path23, node) {
247
+ const parent = path23[path23.length - 1];
248
248
  if (identity.isCollection(parent)) {
249
249
  parent.items[key] = node;
250
250
  } else if (identity.isPair(parent)) {
@@ -854,10 +854,10 @@ var require_Collection = __commonJS({
854
854
  var createNode = require_createNode();
855
855
  var identity = require_identity();
856
856
  var Node = require_Node();
857
- function collectionFromPath(schema, path22, value) {
857
+ function collectionFromPath(schema, path23, value) {
858
858
  let v = value;
859
- for (let i = path22.length - 1; i >= 0; --i) {
860
- const k = path22[i];
859
+ for (let i = path23.length - 1; i >= 0; --i) {
860
+ const k = path23[i];
861
861
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
862
862
  const a = [];
863
863
  a[k] = v;
@@ -876,7 +876,7 @@ var require_Collection = __commonJS({
876
876
  sourceObjects: /* @__PURE__ */ new Map()
877
877
  });
878
878
  }
879
- var isEmptyPath = (path22) => path22 == null || typeof path22 === "object" && !!path22[Symbol.iterator]().next().done;
879
+ var isEmptyPath = (path23) => path23 == null || typeof path23 === "object" && !!path23[Symbol.iterator]().next().done;
880
880
  var Collection = class extends Node.NodeBase {
881
881
  constructor(type, schema) {
882
882
  super(type);
@@ -906,11 +906,11 @@ var require_Collection = __commonJS({
906
906
  * be a Pair instance or a `{ key, value }` object, which may not have a key
907
907
  * that already exists in the map.
908
908
  */
909
- addIn(path22, value) {
910
- if (isEmptyPath(path22))
909
+ addIn(path23, value) {
910
+ if (isEmptyPath(path23))
911
911
  this.add(value);
912
912
  else {
913
- const [key, ...rest] = path22;
913
+ const [key, ...rest] = path23;
914
914
  const node = this.get(key, true);
915
915
  if (identity.isCollection(node))
916
916
  node.addIn(rest, value);
@@ -924,8 +924,8 @@ var require_Collection = __commonJS({
924
924
  * Removes a value from the collection.
925
925
  * @returns `true` if the item was found and removed.
926
926
  */
927
- deleteIn(path22) {
928
- const [key, ...rest] = path22;
927
+ deleteIn(path23) {
928
+ const [key, ...rest] = path23;
929
929
  if (rest.length === 0)
930
930
  return this.delete(key);
931
931
  const node = this.get(key, true);
@@ -939,8 +939,8 @@ var require_Collection = __commonJS({
939
939
  * scalar values from their surrounding node; to disable set `keepScalar` to
940
940
  * `true` (collections are always returned intact).
941
941
  */
942
- getIn(path22, keepScalar) {
943
- const [key, ...rest] = path22;
942
+ getIn(path23, keepScalar) {
943
+ const [key, ...rest] = path23;
944
944
  const node = this.get(key, true);
945
945
  if (rest.length === 0)
946
946
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -958,8 +958,8 @@ var require_Collection = __commonJS({
958
958
  /**
959
959
  * Checks if the collection includes a value with the key `key`.
960
960
  */
961
- hasIn(path22) {
962
- const [key, ...rest] = path22;
961
+ hasIn(path23) {
962
+ const [key, ...rest] = path23;
963
963
  if (rest.length === 0)
964
964
  return this.has(key);
965
965
  const node = this.get(key, true);
@@ -969,8 +969,8 @@ var require_Collection = __commonJS({
969
969
  * Sets a value in this collection. For `!!set`, `value` needs to be a
970
970
  * boolean to add/remove the item from the set.
971
971
  */
972
- setIn(path22, value) {
973
- const [key, ...rest] = path22;
972
+ setIn(path23, value) {
973
+ const [key, ...rest] = path23;
974
974
  if (rest.length === 0) {
975
975
  this.set(key, value);
976
976
  } else {
@@ -3485,9 +3485,9 @@ var require_Document = __commonJS({
3485
3485
  this.contents.add(value);
3486
3486
  }
3487
3487
  /** Adds a value to the document. */
3488
- addIn(path22, value) {
3488
+ addIn(path23, value) {
3489
3489
  if (assertCollection(this.contents))
3490
- this.contents.addIn(path22, value);
3490
+ this.contents.addIn(path23, value);
3491
3491
  }
3492
3492
  /**
3493
3493
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -3562,14 +3562,14 @@ var require_Document = __commonJS({
3562
3562
  * Removes a value from the document.
3563
3563
  * @returns `true` if the item was found and removed.
3564
3564
  */
3565
- deleteIn(path22) {
3566
- if (Collection.isEmptyPath(path22)) {
3565
+ deleteIn(path23) {
3566
+ if (Collection.isEmptyPath(path23)) {
3567
3567
  if (this.contents == null)
3568
3568
  return false;
3569
3569
  this.contents = null;
3570
3570
  return true;
3571
3571
  }
3572
- return assertCollection(this.contents) ? this.contents.deleteIn(path22) : false;
3572
+ return assertCollection(this.contents) ? this.contents.deleteIn(path23) : false;
3573
3573
  }
3574
3574
  /**
3575
3575
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -3584,10 +3584,10 @@ var require_Document = __commonJS({
3584
3584
  * scalar values from their surrounding node; to disable set `keepScalar` to
3585
3585
  * `true` (collections are always returned intact).
3586
3586
  */
3587
- getIn(path22, keepScalar) {
3588
- if (Collection.isEmptyPath(path22))
3587
+ getIn(path23, keepScalar) {
3588
+ if (Collection.isEmptyPath(path23))
3589
3589
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3590
- return identity.isCollection(this.contents) ? this.contents.getIn(path22, keepScalar) : void 0;
3590
+ return identity.isCollection(this.contents) ? this.contents.getIn(path23, keepScalar) : void 0;
3591
3591
  }
3592
3592
  /**
3593
3593
  * Checks if the document includes a value with the key `key`.
@@ -3598,10 +3598,10 @@ var require_Document = __commonJS({
3598
3598
  /**
3599
3599
  * Checks if the document includes a value at `path`.
3600
3600
  */
3601
- hasIn(path22) {
3602
- if (Collection.isEmptyPath(path22))
3601
+ hasIn(path23) {
3602
+ if (Collection.isEmptyPath(path23))
3603
3603
  return this.contents !== void 0;
3604
- return identity.isCollection(this.contents) ? this.contents.hasIn(path22) : false;
3604
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path23) : false;
3605
3605
  }
3606
3606
  /**
3607
3607
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -3618,13 +3618,13 @@ var require_Document = __commonJS({
3618
3618
  * Sets a value in this document. For `!!set`, `value` needs to be a
3619
3619
  * boolean to add/remove the item from the set.
3620
3620
  */
3621
- setIn(path22, value) {
3622
- if (Collection.isEmptyPath(path22)) {
3621
+ setIn(path23, value) {
3622
+ if (Collection.isEmptyPath(path23)) {
3623
3623
  this.contents = value;
3624
3624
  } else if (this.contents == null) {
3625
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path22), value);
3625
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path23), value);
3626
3626
  } else if (assertCollection(this.contents)) {
3627
- this.contents.setIn(path22, value);
3627
+ this.contents.setIn(path23, value);
3628
3628
  }
3629
3629
  }
3630
3630
  /**
@@ -5584,9 +5584,9 @@ var require_cst_visit = __commonJS({
5584
5584
  visit.BREAK = BREAK;
5585
5585
  visit.SKIP = SKIP;
5586
5586
  visit.REMOVE = REMOVE;
5587
- visit.itemAtPath = (cst, path22) => {
5587
+ visit.itemAtPath = (cst, path23) => {
5588
5588
  let item = cst;
5589
- for (const [field2, index] of path22) {
5589
+ for (const [field2, index] of path23) {
5590
5590
  const tok = item?.[field2];
5591
5591
  if (tok && "items" in tok) {
5592
5592
  item = tok.items[index];
@@ -5595,23 +5595,23 @@ var require_cst_visit = __commonJS({
5595
5595
  }
5596
5596
  return item;
5597
5597
  };
5598
- visit.parentCollection = (cst, path22) => {
5599
- const parent = visit.itemAtPath(cst, path22.slice(0, -1));
5600
- const field2 = path22[path22.length - 1][0];
5598
+ visit.parentCollection = (cst, path23) => {
5599
+ const parent = visit.itemAtPath(cst, path23.slice(0, -1));
5600
+ const field2 = path23[path23.length - 1][0];
5601
5601
  const coll = parent?.[field2];
5602
5602
  if (coll && "items" in coll)
5603
5603
  return coll;
5604
5604
  throw new Error("Parent collection not found");
5605
5605
  };
5606
- function _visit(path22, item, visitor) {
5607
- let ctrl = visitor(item, path22);
5606
+ function _visit(path23, item, visitor) {
5607
+ let ctrl = visitor(item, path23);
5608
5608
  if (typeof ctrl === "symbol")
5609
5609
  return ctrl;
5610
5610
  for (const field2 of ["key", "value"]) {
5611
5611
  const token = item[field2];
5612
5612
  if (token && "items" in token) {
5613
5613
  for (let i = 0; i < token.items.length; ++i) {
5614
- const ci = _visit(Object.freeze(path22.concat([[field2, i]])), token.items[i], visitor);
5614
+ const ci = _visit(Object.freeze(path23.concat([[field2, i]])), token.items[i], visitor);
5615
5615
  if (typeof ci === "number")
5616
5616
  i = ci - 1;
5617
5617
  else if (ci === BREAK)
@@ -5622,10 +5622,10 @@ var require_cst_visit = __commonJS({
5622
5622
  }
5623
5623
  }
5624
5624
  if (typeof ctrl === "function" && field2 === "key")
5625
- ctrl = ctrl(item, path22);
5625
+ ctrl = ctrl(item, path23);
5626
5626
  }
5627
5627
  }
5628
- return typeof ctrl === "function" ? ctrl(item, path22) : ctrl;
5628
+ return typeof ctrl === "function" ? ctrl(item, path23) : ctrl;
5629
5629
  }
5630
5630
  exports.visit = visit;
5631
5631
  }
@@ -6927,14 +6927,14 @@ var require_parser = __commonJS({
6927
6927
  case "scalar":
6928
6928
  case "single-quoted-scalar":
6929
6929
  case "double-quoted-scalar": {
6930
- const fs21 = this.flowScalar(this.type);
6930
+ const fs22 = this.flowScalar(this.type);
6931
6931
  if (atNextItem || it.value) {
6932
- map.items.push({ start, key: fs21, sep: [] });
6932
+ map.items.push({ start, key: fs22, sep: [] });
6933
6933
  this.onKeyLine = true;
6934
6934
  } else if (it.sep) {
6935
- this.stack.push(fs21);
6935
+ this.stack.push(fs22);
6936
6936
  } else {
6937
- Object.assign(it, { key: fs21, sep: [] });
6937
+ Object.assign(it, { key: fs22, sep: [] });
6938
6938
  this.onKeyLine = true;
6939
6939
  }
6940
6940
  return;
@@ -7062,13 +7062,13 @@ var require_parser = __commonJS({
7062
7062
  case "scalar":
7063
7063
  case "single-quoted-scalar":
7064
7064
  case "double-quoted-scalar": {
7065
- const fs21 = this.flowScalar(this.type);
7065
+ const fs22 = this.flowScalar(this.type);
7066
7066
  if (!it || it.value)
7067
- fc.items.push({ start: [], key: fs21, sep: [] });
7067
+ fc.items.push({ start: [], key: fs22, sep: [] });
7068
7068
  else if (it.sep)
7069
- this.stack.push(fs21);
7069
+ this.stack.push(fs22);
7070
7070
  else
7071
- Object.assign(it, { key: fs21, sep: [] });
7071
+ Object.assign(it, { key: fs22, sep: [] });
7072
7072
  return;
7073
7073
  }
7074
7074
  case "flow-map-end":
@@ -7257,7 +7257,7 @@ var require_public_api = __commonJS({
7257
7257
  return docs;
7258
7258
  return Object.assign([], { empty: true }, composer$1.streamInfo());
7259
7259
  }
7260
- function parseDocument7(source, options = {}) {
7260
+ function parseDocument8(source, options = {}) {
7261
7261
  const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
7262
7262
  const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
7263
7263
  const composer$1 = new composer.Composer(options);
@@ -7283,7 +7283,7 @@ var require_public_api = __commonJS({
7283
7283
  } else if (options === void 0 && reviver && typeof reviver === "object") {
7284
7284
  options = reviver;
7285
7285
  }
7286
- const doc = parseDocument7(src, options);
7286
+ const doc = parseDocument8(src, options);
7287
7287
  if (!doc)
7288
7288
  return null;
7289
7289
  doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
@@ -7319,7 +7319,7 @@ var require_public_api = __commonJS({
7319
7319
  }
7320
7320
  exports.parse = parse2;
7321
7321
  exports.parseAllDocuments = parseAllDocuments;
7322
- exports.parseDocument = parseDocument7;
7322
+ exports.parseDocument = parseDocument8;
7323
7323
  exports.stringify = stringify;
7324
7324
  }
7325
7325
  });
@@ -7824,6 +7824,7 @@ var CLASSIC_WIRE_KEYS = [
7824
7824
  "tdd_mode",
7825
7825
  "review_mode",
7826
7826
  "isolation",
7827
+ "bound_branch",
7827
7828
  "verify_mode",
7828
7829
  "auto_transition",
7829
7830
  "base_ref",
@@ -7942,6 +7943,7 @@ function classicStateFromDocument(doc) {
7942
7943
  tddMode: enumValue(doc, "tdd_mode", TDD_MODES),
7943
7944
  reviewMode: enumValue(doc, "review_mode", REVIEW_MODES),
7944
7945
  isolation: enumValue(doc, "isolation", ISOLATIONS),
7946
+ boundBranch: nullableString(doc, "bound_branch"),
7945
7947
  verifyMode: enumValue(doc, "verify_mode", VERIFY_MODES),
7946
7948
  autoTransition: booleanValue(doc, "auto_transition"),
7947
7949
  baseRef: nullableString(doc, "base_ref"),
@@ -8007,6 +8009,7 @@ function classicStateToDocument(state) {
8007
8009
  tdd_mode: state.tddMode,
8008
8010
  review_mode: state.reviewMode,
8009
8011
  isolation: state.isolation,
8012
+ bound_branch: state.boundBranch,
8010
8013
  verify_mode: state.verifyMode,
8011
8014
  auto_transition: state.autoTransition,
8012
8015
  base_ref: state.baseRef,
@@ -9493,6 +9496,7 @@ function applyClassicTransition(current, event, options = {}) {
9493
9496
  setField(classic, effects, "tddMode", null);
9494
9497
  setField(classic, effects, "reviewMode", null);
9495
9498
  setField(classic, effects, "isolation", null);
9499
+ setField(classic, effects, "boundBranch", null);
9496
9500
  setField(classic, effects, "verifyMode", null);
9497
9501
  setField(classic, effects, "directOverride", null);
9498
9502
  } else if (event === "archive-confirm") {
@@ -9907,11 +9911,11 @@ var classicArchiveCommand = async (args) => {
9907
9911
  };
9908
9912
 
9909
9913
  // domains/comet-classic/classic-guard.ts
9910
- var import_yaml5 = __toESM(require_dist(), 1);
9914
+ var import_yaml6 = __toESM(require_dist(), 1);
9911
9915
  import { spawnSync as spawnSync2 } from "child_process";
9912
9916
  import { createHash as createHash4 } from "crypto";
9913
- import { existsSync, promises as fs15, readFileSync } from "fs";
9914
- import path16 from "path";
9917
+ import { existsSync, promises as fs16, readFileSync } from "fs";
9918
+ import path17 from "path";
9915
9919
 
9916
9920
  // domains/comet-classic/classic-command-checks.ts
9917
9921
  import path13 from "path";
@@ -10213,6 +10217,12 @@ var classicValidateCommand = async (args) => {
10213
10217
  fail3(`${field2}='${value}' is not valid. Expected: ${values.join(" ")}`);
10214
10218
  }
10215
10219
  }
10220
+ if (Object.prototype.hasOwnProperty.call(record, "bound_branch")) {
10221
+ const value = record.bound_branch;
10222
+ if (value !== null && typeof value !== "string") {
10223
+ fail3(`bound_branch='${text(value)}' is not a string or null`);
10224
+ }
10225
+ }
10216
10226
  if (Object.prototype.hasOwnProperty.call(record, "verify_failures")) {
10217
10227
  const value = record.verify_failures;
10218
10228
  if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
@@ -10303,6 +10313,96 @@ async function readClassicConfigValue(field2, options = {}) {
10303
10313
  return null;
10304
10314
  }
10305
10315
 
10316
+ // domains/comet-classic/classic-branch-binding.ts
10317
+ var import_yaml5 = __toESM(require_dist(), 1);
10318
+ import { execFileSync } from "child_process";
10319
+ import { randomUUID as randomUUID6 } from "crypto";
10320
+ import { promises as fs15 } from "fs";
10321
+ import path16 from "path";
10322
+ function liveGitBranch(cwd) {
10323
+ try {
10324
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
10325
+ cwd,
10326
+ encoding: "utf8",
10327
+ stdio: ["ignore", "pipe", "ignore"]
10328
+ }).trim();
10329
+ return branch && branch !== "HEAD" ? branch : null;
10330
+ } catch {
10331
+ return null;
10332
+ }
10333
+ }
10334
+ function isGitWorkTree(cwd) {
10335
+ try {
10336
+ return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
10337
+ cwd,
10338
+ encoding: "utf8",
10339
+ stdio: ["ignore", "pipe", "ignore"]
10340
+ }).trim() === "true";
10341
+ } catch {
10342
+ return false;
10343
+ }
10344
+ }
10345
+ var BOUND_BRANCH_ISOLATIONS = ["current", "branch", "worktree"];
10346
+ function requiresBranchBinding(isolation) {
10347
+ return BOUND_BRANCH_ISOLATIONS.includes(isolation);
10348
+ }
10349
+ function evaluateBranchBinding(input) {
10350
+ if (!requiresBranchBinding(input.isolation)) return { status: "not-applicable" };
10351
+ if (input.boundBranch === null && input.currentBranch === null && input.gitWorkTree === false) {
10352
+ return { status: "not-applicable" };
10353
+ }
10354
+ if (input.boundBranch === null) {
10355
+ return input.currentBranch === null ? { status: "unbound-detached" } : { status: "needs-heal", branch: input.currentBranch };
10356
+ }
10357
+ if (input.currentBranch === input.boundBranch) return { status: "ok" };
10358
+ return { status: "drift", boundBranch: input.boundBranch, currentBranch: input.currentBranch };
10359
+ }
10360
+ async function resolveBranchBinding(changeDir, options) {
10361
+ const file = path16.join(changeDir, ".comet.yaml");
10362
+ const document = (0, import_yaml5.parseDocument)(await fs15.readFile(file, "utf8"), { uniqueKeys: false });
10363
+ if (document.errors.length > 0) {
10364
+ throw new Error(`Invalid .comet.yaml: ${document.errors[0].message}`);
10365
+ }
10366
+ const record = document.toJS() ?? {};
10367
+ const isolation = typeof record.isolation === "string" ? record.isolation : null;
10368
+ const boundBranch = typeof record.bound_branch === "string" && record.bound_branch !== "" ? record.bound_branch : null;
10369
+ const bindingRequired = requiresBranchBinding(isolation);
10370
+ const currentBranch = liveGitBranch(options.cwd);
10371
+ const gitWorkTree = bindingRequired && boundBranch === null && currentBranch === null ? isGitWorkTree(options.cwd) : true;
10372
+ const verdict = evaluateBranchBinding({ isolation, boundBranch, currentBranch, gitWorkTree });
10373
+ if (verdict.status === "needs-heal" && options.heal) {
10374
+ await healBoundBranch(changeDir, verdict.branch);
10375
+ return { status: "healed", branch: verdict.branch, bindingRequired, currentBranch };
10376
+ }
10377
+ return { ...verdict, bindingRequired, currentBranch };
10378
+ }
10379
+ async function healBoundBranch(changeDir, branch) {
10380
+ const file = path16.join(changeDir, ".comet.yaml");
10381
+ const document = (0, import_yaml5.parseDocument)(await fs15.readFile(file, "utf8"), { uniqueKeys: false });
10382
+ document.set("bound_branch", branch);
10383
+ const temporary = `${file}.${randomUUID6()}.tmp`;
10384
+ try {
10385
+ await fs15.writeFile(temporary, document.toString(), "utf8");
10386
+ await fs15.rename(temporary, file);
10387
+ } catch (error) {
10388
+ await fs15.rm(temporary, { force: true });
10389
+ throw error;
10390
+ }
10391
+ }
10392
+ function branchLabel(currentBranch) {
10393
+ return currentBranch ?? "detached HEAD";
10394
+ }
10395
+ function driftBlockedMessage(change, boundBranch, currentBranch) {
10396
+ return `change '${change}' is bound to branch '${boundBranch}', but current branch is '${branchLabel(currentBranch)}'.
10397
+ Next: ask the user to confirm — switch back to '${boundBranch}', or run \`comet state rebind ${change}\` after explicit confirmation.`;
10398
+ }
10399
+ function driftStaleReason(change, boundBranch, currentBranch) {
10400
+ return `change '${change}' is bound to branch '${boundBranch}', but current branch is '${branchLabel(currentBranch)}'`;
10401
+ }
10402
+ function unboundDetachedMessage(change) {
10403
+ return `change '${change}' uses a branch-bound workspace mode but has no bound branch and HEAD is detached; checkout a branch first before continuing.`;
10404
+ }
10405
+
10306
10406
  // domains/comet-classic/classic-guard.ts
10307
10407
  var GREEN3 = "\x1B[32m";
10308
10408
  var RED3 = "\x1B[31m";
@@ -10367,7 +10467,7 @@ var GuardOutput = class {
10367
10467
  };
10368
10468
  async function exists4(file) {
10369
10469
  try {
10370
- await fs15.access(file);
10470
+ await fs16.access(file);
10371
10471
  return true;
10372
10472
  } catch (error) {
10373
10473
  if (error.code === "ENOENT") return false;
@@ -10376,7 +10476,7 @@ async function exists4(file) {
10376
10476
  }
10377
10477
  async function nonempty(file) {
10378
10478
  try {
10379
- return (await fs15.stat(file)).size > 0;
10479
+ return (await fs16.stat(file)).size > 0;
10380
10480
  } catch (error) {
10381
10481
  if (error.code === "ENOENT") return false;
10382
10482
  throw error;
@@ -10390,8 +10490,8 @@ async function resolveChangeDir(name) {
10390
10490
  return (await resolveClassicChangeDirectory(name)).label;
10391
10491
  }
10392
10492
  async function readField(changeDir, field2) {
10393
- const file = path16.join(changeDir, ".comet.yaml");
10394
- const document = (0, import_yaml5.parseDocument)(await fs15.readFile(file, "utf8"), { uniqueKeys: false });
10493
+ const file = path17.join(changeDir, ".comet.yaml");
10494
+ const document = (0, import_yaml6.parseDocument)(await fs16.readFile(file, "utf8"), { uniqueKeys: false });
10395
10495
  if (document.errors.length > 0) {
10396
10496
  throw new GuardFailure(`ERROR: Invalid .comet.yaml: ${document.errors[0].message}`);
10397
10497
  }
@@ -10432,7 +10532,7 @@ function countEnglishWords(source) {
10432
10532
  }
10433
10533
  async function documentLanguageMatchesConfigured(changeDir, file) {
10434
10534
  const language = await configuredLanguage(changeDir);
10435
- const source = stripFencedCodeBlocks(await fs15.readFile(file, "utf8"));
10535
+ const source = stripFencedCodeBlocks(await fs16.readFile(file, "utf8"));
10436
10536
  const cjk = countCjkChars(source);
10437
10537
  const englishWords = countEnglishWords(source);
10438
10538
  if (language === "zh-CN" && cjk < 20 && englishWords >= 20) {
@@ -10456,7 +10556,7 @@ async function handoffSourceFiles(changeDir) {
10456
10556
  const files = [`${changeDir}/proposal.md`, `${changeDir}/design.md`, `${changeDir}/tasks.md`];
10457
10557
  const specs = `${changeDir}/specs`;
10458
10558
  if (await exists4(specs)) {
10459
- for (const entry2 of (await fs15.readdir(specs)).sort()) {
10559
+ for (const entry2 of (await fs16.readdir(specs)).sort()) {
10460
10560
  const spec = `${specs}/${entry2}/spec.md`;
10461
10561
  if (await exists4(spec)) files.push(spec);
10462
10562
  }
@@ -10476,7 +10576,7 @@ async function preflight(changeDir, name) {
10476
10576
  if (!await exists4(changeDir)) {
10477
10577
  throw new GuardFailure(red2(`FATAL: change directory not found: ${changeDir}`));
10478
10578
  }
10479
- if (!await exists4(path16.join(changeDir, ".comet.yaml"))) {
10579
+ if (!await exists4(path17.join(changeDir, ".comet.yaml"))) {
10480
10580
  throw new GuardFailure(red2(`FATAL: .comet.yaml not found in ${changeDir}`));
10481
10581
  }
10482
10582
  const result5 = await classicValidateCommand([name], { json: false });
@@ -10546,9 +10646,9 @@ var INFERRED_COMMAND_SOURCES = [
10546
10646
  "Cargo.toml"
10547
10647
  ];
10548
10648
  async function removedProjectCommandField(field2) {
10549
- const config = path16.join(".comet", "config.yaml");
10649
+ const config = path17.join(".comet", "config.yaml");
10550
10650
  if (!await exists4(config)) return false;
10551
- const document = (0, import_yaml5.parseDocument)(await fs15.readFile(config, "utf8"));
10651
+ const document = (0, import_yaml6.parseDocument)(await fs16.readFile(config, "utf8"));
10552
10652
  if (document.errors.length > 0) {
10553
10653
  throw new Error(
10554
10654
  `.comet/config.yaml is invalid YAML (${document.errors[0].message}); cannot check for removed "${field2}" field. Fix the config and retry.`
@@ -10629,14 +10729,14 @@ ${recoveryCommand(change, scope, recorded.command)}`
10629
10729
  return { status: 0, output: evidenceDetail(recorded) };
10630
10730
  }
10631
10731
  async function tasksAllDone(changeDir) {
10632
- const tasks = path16.join(changeDir, "tasks.md");
10732
+ const tasks = path17.join(changeDir, "tasks.md");
10633
10733
  if (!await exists4(tasks)) {
10634
10734
  return fail(
10635
10735
  `tasks.md is missing at ${tasks}
10636
10736
  Next: restore or create tasks.md for this change before leaving build.`
10637
10737
  );
10638
10738
  }
10639
- const source = await fs15.readFile(tasks, "utf8");
10739
+ const source = await fs16.readFile(tasks, "utf8");
10640
10740
  if (!/- \[x\]/u.test(source)) {
10641
10741
  return fail(
10642
10742
  "tasks.md has no completed tasks.\nNext: complete implementation tasks and mark them with '- [x]'."
@@ -10653,9 +10753,9 @@ Next: complete or explicitly remove unfinished tasks, then mark tasks.md with '-
10653
10753
  return pass();
10654
10754
  }
10655
10755
  async function tasksHasAny(changeDir) {
10656
- const tasks = path16.join(changeDir, "tasks.md");
10756
+ const tasks = path17.join(changeDir, "tasks.md");
10657
10757
  if (!await exists4(tasks)) return false;
10658
- return /- \[/u.test(await fs15.readFile(tasks, "utf8"));
10758
+ return /- \[/u.test(await fs16.readFile(tasks, "utf8"));
10659
10759
  }
10660
10760
  async function planTasksAllDone(changeDir) {
10661
10761
  const plan = await readField(changeDir, "plan");
@@ -10666,7 +10766,7 @@ async function planTasksAllDone(changeDir) {
10666
10766
  Next: restore the Superpowers plan file or update .comet.yaml plan before leaving build.`
10667
10767
  );
10668
10768
  }
10669
- const source = await fs15.readFile(plan, "utf8");
10769
+ const source = await fs16.readFile(plan, "utf8");
10670
10770
  const unfinished = source.split(/\r?\n/u).map((line, index) => ({ line, number: index + 1 })).filter((entry2) => /^\s*- \[ \]/u.test(entry2.line));
10671
10771
  if (unfinished.length > 0) {
10672
10772
  return fail(
@@ -10677,14 +10777,36 @@ Next: check off corresponding completed plan tasks, then commit the plan update.
10677
10777
  }
10678
10778
  return pass();
10679
10779
  }
10780
+ async function boundBranchMatches(changeDir, change) {
10781
+ let outcome;
10782
+ try {
10783
+ outcome = await resolveBranchBinding(changeDir, { heal: true, cwd: process.cwd() });
10784
+ } catch (error) {
10785
+ throw new GuardFailure(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
10786
+ }
10787
+ switch (outcome.status) {
10788
+ case "drift":
10789
+ return fail(driftBlockedMessage(change, outcome.boundBranch, outcome.currentBranch));
10790
+ case "unbound-detached":
10791
+ return fail(unboundDetachedMessage(change));
10792
+ case "healed":
10793
+ return pass(`bound_branch lazily set to ${outcome.branch}`);
10794
+ case "needs-heal":
10795
+ case "ok":
10796
+ case "not-applicable":
10797
+ return pass();
10798
+ default: {
10799
+ const exhaustive = outcome;
10800
+ throw new Error(`unhandled branch binding status: ${JSON.stringify(exhaustive)}`);
10801
+ }
10802
+ }
10803
+ }
10680
10804
  async function isolationSelected(changeDir, change) {
10681
10805
  const isolation = await readField(changeDir, "isolation");
10682
- const workflow = await readField(changeDir, "workflow");
10683
- if (isolation === "branch" || isolation === "worktree") return pass();
10684
- if (isolation === "current" && (workflow === "hotfix" || workflow === "tweak")) return pass();
10685
- const allowedValues = workflow === "full" ? "<branch|worktree>" : "<current|branch|worktree>";
10806
+ if (isolation === "current" || isolation === "branch" || isolation === "worktree") return pass();
10807
+ const allowedValues = "<current|branch|worktree>";
10686
10808
  return fail(
10687
- `isolation must be ${workflow === "full" ? "branch or worktree" : "current, branch, or worktree"}, got '${isolation || "null"}'
10809
+ `isolation must be current, branch, or worktree, got '${isolation || "null"}'
10688
10810
  Next: choose a valid workspace mode, prepare it when needed, then run:
10689
10811
  comet state set ${change} isolation ${allowedValues}`
10690
10812
  );
@@ -10758,7 +10880,7 @@ async function archivedIsTrue(changeDir) {
10758
10880
  return await readField(changeDir, "archived") === "true";
10759
10881
  }
10760
10882
  async function designDocFrontmatterHas(designDoc, field2, expected) {
10761
- const source = (await fs15.readFile(designDoc, "utf8")).replace(/^\uFEFF/u, "");
10883
+ const source = (await fs16.readFile(designDoc, "utf8")).replace(/^\uFEFF/u, "");
10762
10884
  let inFrontmatter = false;
10763
10885
  for (const line of source.split(/\r?\n/u)) {
10764
10886
  if (!inFrontmatter) {
@@ -10823,7 +10945,7 @@ async function designHandoffMarkdownTraceable(changeDir) {
10823
10945
  const markdown = `${context.replace(/\.json$/u, "")}.md`;
10824
10946
  if (!await nonempty(markdown))
10825
10947
  return fail(`design handoff markdown is missing or empty: ${markdown}`);
10826
- const source = await fs15.readFile(markdown, "utf8");
10948
+ const source = await fs16.readFile(markdown, "utf8");
10827
10949
  const problems = [];
10828
10950
  if (!/^Generated-by: comet-handoff\.sh$/mu.test(source)) {
10829
10951
  problems.push("handoff markdown is missing Generated-by marker");
@@ -10850,7 +10972,7 @@ async function betaSpecJsonStructurallyValid(changeDir) {
10850
10972
  const context = await readField(changeDir, "handoff_context");
10851
10973
  if (!context || context === "null") return fail("handoff_context is missing from .comet.yaml");
10852
10974
  if (!await nonempty(context)) return fail(`spec-context.json is missing or empty: ${context}`);
10853
- const source = await fs15.readFile(context, "utf8");
10975
+ const source = await fs16.readFile(context, "utf8");
10854
10976
  const problems = [];
10855
10977
  let parsed;
10856
10978
  try {
@@ -10887,19 +11009,19 @@ async function guardOpenChecks(output, changeDir) {
10887
11009
  const checks = [
10888
11010
  check(
10889
11011
  "proposal.md exists and non-empty",
10890
- async () => await nonempty(path16.join(changeDir, "proposal.md")) ? pass() : fail("")
11012
+ async () => await nonempty(path17.join(changeDir, "proposal.md")) ? pass() : fail("")
10891
11013
  ),
10892
11014
  check(
10893
11015
  "proposal.md matches configured language",
10894
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "proposal.md"))
11016
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "proposal.md"))
10895
11017
  ),
10896
11018
  check(
10897
11019
  "tasks.md exists and non-empty",
10898
- async () => await nonempty(path16.join(changeDir, "tasks.md")) ? pass() : fail("")
11020
+ async () => await nonempty(path17.join(changeDir, "tasks.md")) ? pass() : fail("")
10899
11021
  ),
10900
11022
  check(
10901
11023
  "tasks.md matches configured language",
10902
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "tasks.md"))
11024
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "tasks.md"))
10903
11025
  ),
10904
11026
  check(
10905
11027
  "tasks.md has at least one task",
@@ -10912,11 +11034,11 @@ async function guardOpenChecks(output, changeDir) {
10912
11034
  0,
10913
11035
  check(
10914
11036
  "design.md exists and non-empty",
10915
- async () => await nonempty(path16.join(changeDir, "design.md")) ? pass() : fail("")
11037
+ async () => await nonempty(path17.join(changeDir, "design.md")) ? pass() : fail("")
10916
11038
  ),
10917
11039
  check(
10918
11040
  "design.md matches configured language",
10919
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "design.md"))
11041
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "design.md"))
10920
11042
  )
10921
11043
  );
10922
11044
  }
@@ -10928,27 +11050,27 @@ async function guardDesignChecks(output, changeDir, change) {
10928
11050
  const builders = [
10929
11051
  check(
10930
11052
  "proposal.md exists",
10931
- async () => await nonempty(path16.join(changeDir, "proposal.md")) ? pass() : fail("")
11053
+ async () => await nonempty(path17.join(changeDir, "proposal.md")) ? pass() : fail("")
10932
11054
  ),
10933
11055
  check(
10934
11056
  "proposal.md matches configured language",
10935
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "proposal.md"))
11057
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "proposal.md"))
10936
11058
  ),
10937
11059
  check(
10938
11060
  "design.md exists",
10939
- async () => await nonempty(path16.join(changeDir, "design.md")) ? pass() : fail("")
11061
+ async () => await nonempty(path17.join(changeDir, "design.md")) ? pass() : fail("")
10940
11062
  ),
10941
11063
  check(
10942
11064
  "design.md matches configured language",
10943
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "design.md"))
11065
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "design.md"))
10944
11066
  ),
10945
11067
  check(
10946
11068
  "tasks.md exists",
10947
- async () => await nonempty(path16.join(changeDir, "tasks.md")) ? pass() : fail("")
11069
+ async () => await nonempty(path17.join(changeDir, "tasks.md")) ? pass() : fail("")
10948
11070
  ),
10949
11071
  check(
10950
11072
  "tasks.md matches configured language",
10951
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "tasks.md"))
11073
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "tasks.md"))
10952
11074
  ),
10953
11075
  check("design handoff context exists", () => designHandoffContextValid(changeDir, change)),
10954
11076
  check("design handoff markdown is traceable", () => designHandoffMarkdownTraceable(changeDir))
@@ -10999,6 +11121,7 @@ async function guardDesignChecks(output, changeDir, change) {
10999
11121
  }
11000
11122
  async function guardBuildChecks(output, changeDir, change, run) {
11001
11123
  return runChecks(output, [
11124
+ check("bound branch matches workspace mode", () => boundBranchMatches(changeDir, change)),
11002
11125
  check("isolation selected", () => isolationSelected(changeDir, change)),
11003
11126
  check("build_mode selected", () => buildModeSelected(changeDir, change)),
11004
11127
  check("build_mode allowed for workflow", () => buildModeAllowedForWorkflow(changeDir)),
@@ -11009,11 +11132,11 @@ async function guardBuildChecks(output, changeDir, change, run) {
11009
11132
  check("Superpowers plan all tasks checked", () => planTasksAllDone(changeDir)),
11010
11133
  check(
11011
11134
  "proposal.md exists",
11012
- async () => await nonempty(path16.join(changeDir, "proposal.md")) ? pass() : fail("")
11135
+ async () => await nonempty(path17.join(changeDir, "proposal.md")) ? pass() : fail("")
11013
11136
  ),
11014
11137
  check(
11015
11138
  "proposal.md matches configured language",
11016
- () => documentLanguageMatchesConfigured(changeDir, path16.join(changeDir, "proposal.md"))
11139
+ () => documentLanguageMatchesConfigured(changeDir, path17.join(changeDir, "proposal.md"))
11017
11140
  ),
11018
11141
  check("Superpowers plan matches configured language", async () => {
11019
11142
  const plan = await readField(changeDir, "plan");
@@ -11030,6 +11153,7 @@ async function guardBuildChecks(output, changeDir, change, run) {
11030
11153
  }
11031
11154
  async function guardVerifyChecks(output, changeDir, change, run) {
11032
11155
  return runChecks(output, [
11156
+ check("bound branch matches workspace mode", () => boundBranchMatches(changeDir, change)),
11033
11157
  check("tasks.md all tasks checked", () => tasksAllDone(changeDir)),
11034
11158
  // Verification command runs after tasks check — no point running tests
11035
11159
  // if tasks.md is incomplete.
@@ -11048,16 +11172,17 @@ async function guardVerifyChecks(output, changeDir, change, run) {
11048
11172
  })
11049
11173
  ]);
11050
11174
  }
11051
- async function guardArchiveChecks(output, changeDir) {
11175
+ async function guardArchiveChecks(output, changeDir, change) {
11052
11176
  return runChecks(output, [
11177
+ check("bound branch matches workspace mode", () => boundBranchMatches(changeDir, change)),
11053
11178
  check("archived is true", async () => await archivedIsTrue(changeDir) ? pass() : fail("")),
11054
11179
  check(
11055
11180
  "proposal.md exists",
11056
- async () => await nonempty(path16.join(changeDir, "proposal.md")) ? pass() : fail("")
11181
+ async () => await nonempty(path17.join(changeDir, "proposal.md")) ? pass() : fail("")
11057
11182
  ),
11058
11183
  check(
11059
11184
  "design.md exists",
11060
- async () => await nonempty(path16.join(changeDir, "design.md")) ? pass() : fail("")
11185
+ async () => await nonempty(path17.join(changeDir, "design.md")) ? pass() : fail("")
11061
11186
  ),
11062
11187
  check("tasks.md all tasks checked", () => tasksAllDone(changeDir)),
11063
11188
  check(
@@ -11066,9 +11191,10 @@ async function guardArchiveChecks(output, changeDir) {
11066
11191
  )
11067
11192
  ]);
11068
11193
  }
11069
- async function applyStateUpdate(output, change, changeDir, phase, context) {
11194
+ async function applyStateUpdate(output, change, changeDir, phase) {
11070
11195
  const event = CLASSIC_GUARD_TRANSITION_EVENT[phase];
11071
11196
  if (!event) return;
11197
+ const context = await ensureClassicRuntimeRun(changeDir);
11072
11198
  const result5 = applyClassicTransition(context.classic, event);
11073
11199
  await transitionClassicRuntimeRun(changeDir, result5.classic, context.run, {
11074
11200
  event,
@@ -11122,7 +11248,7 @@ Valid phases: open, design, build, verify, archive`
11122
11248
  blocked2 = await guardBuildChecks(output, changeDir, change, runContext.run);
11123
11249
  else if (phase === "verify")
11124
11250
  blocked2 = await guardVerifyChecks(output, changeDir, change, runContext.run);
11125
- else blocked2 = await guardArchiveChecks(output, changeDir);
11251
+ else blocked2 = await guardArchiveChecks(output, changeDir, change);
11126
11252
  if (blocked2) {
11127
11253
  output.stderr.push("");
11128
11254
  output.stderr.push(red2("BLOCKED — fix failing checks before proceeding to next phase"));
@@ -11131,7 +11257,7 @@ Valid phases: open, design, build, verify, archive`
11131
11257
  output.stderr.push("");
11132
11258
  output.stderr.push(green2("ALL CHECKS PASSED — ready for next phase"));
11133
11259
  if (flag === "--apply") {
11134
- await applyStateUpdate(output, change, changeDir, phase, runContext);
11260
+ await applyStateUpdate(output, change, changeDir, phase);
11135
11261
  }
11136
11262
  return output.toResult(0);
11137
11263
  } catch (error) {
@@ -11144,10 +11270,10 @@ Valid phases: open, design, build, verify, archive`
11144
11270
  };
11145
11271
 
11146
11272
  // domains/comet-classic/classic-handoff.ts
11147
- var import_yaml6 = __toESM(require_dist(), 1);
11273
+ var import_yaml7 = __toESM(require_dist(), 1);
11148
11274
  import { createHash as createHash5 } from "crypto";
11149
- import { promises as fs16, readFileSync as readFileSync2 } from "fs";
11150
- import path17 from "path";
11275
+ import { promises as fs17, readFileSync as readFileSync2 } from "fs";
11276
+ import path18 from "path";
11151
11277
  var GREEN4 = "\x1B[32m";
11152
11278
  var RED4 = "\x1B[31m";
11153
11279
  var YELLOW4 = "\x1B[33m";
@@ -11181,7 +11307,7 @@ var HandoffOutput = class {
11181
11307
  };
11182
11308
  async function exists5(file) {
11183
11309
  try {
11184
- await fs16.access(file);
11310
+ await fs17.access(file);
11185
11311
  return true;
11186
11312
  } catch (error) {
11187
11313
  if (error.code === "ENOENT") return false;
@@ -11190,7 +11316,7 @@ async function exists5(file) {
11190
11316
  }
11191
11317
  async function nonempty2(file) {
11192
11318
  try {
11193
- return (await fs16.stat(file)).size > 0;
11319
+ return (await fs17.stat(file)).size > 0;
11194
11320
  } catch (error) {
11195
11321
  if (error.code === "ENOENT") return false;
11196
11322
  throw error;
@@ -11219,7 +11345,7 @@ async function handoffSourceFiles2(changeDir) {
11219
11345
  const files = [`${changeDir}/proposal.md`, `${changeDir}/design.md`, `${changeDir}/tasks.md`];
11220
11346
  const specs = `${changeDir}/specs`;
11221
11347
  if (await exists5(specs)) {
11222
- for (const entry2 of (await fs16.readdir(specs)).sort()) {
11348
+ for (const entry2 of (await fs17.readdir(specs)).sort()) {
11223
11349
  const spec = `${specs}/${entry2}/spec.md`;
11224
11350
  if (await exists5(spec)) files.push(spec);
11225
11351
  }
@@ -11267,7 +11393,7 @@ async function writeMarkdownContext(changeDir, change, mode, contextHash, output
11267
11393
  ];
11268
11394
  for (const file of await handoffSourceFiles2(changeDir)) {
11269
11395
  if (!await exists5(file)) continue;
11270
- const content = await fs16.readFile(file, "utf8");
11396
+ const content = await fs17.readFile(file, "utf8");
11271
11397
  const total = lineCount(content);
11272
11398
  lines.push(
11273
11399
  `## ${file}`,
@@ -11292,7 +11418,7 @@ async function writeMarkdownContext(changeDir, change, mode, contextHash, output
11292
11418
  }
11293
11419
  lines.push("");
11294
11420
  }
11295
- await fs16.writeFile(output, lines.join("\n"));
11421
+ await fs17.writeFile(output, lines.join("\n"));
11296
11422
  }
11297
11423
  async function writeJsonContext(changeDir, change, mode, contextHash, output) {
11298
11424
  const entries = [];
@@ -11315,7 +11441,7 @@ async function writeJsonContext(changeDir, change, mode, contextHash, output) {
11315
11441
  "}",
11316
11442
  ""
11317
11443
  ].join("\n");
11318
- await fs16.writeFile(output, document);
11444
+ await fs17.writeFile(output, document);
11319
11445
  }
11320
11446
  async function writeSpecProjectionForFile(file, content) {
11321
11447
  return [
@@ -11355,11 +11481,11 @@ async function writeSpecMarkdownContext(changeDir, change, contextHash, output)
11355
11481
  const specs = `${changeDir}/specs`;
11356
11482
  let projected = false;
11357
11483
  if (await exists5(specs)) {
11358
- for (const entry2 of (await fs16.readdir(specs)).sort()) {
11484
+ for (const entry2 of (await fs17.readdir(specs)).sort()) {
11359
11485
  const spec = `${specs}/${entry2}/spec.md`;
11360
11486
  if (!await exists5(spec)) continue;
11361
11487
  projected = true;
11362
- lines.push(...await writeSpecProjectionForFile(spec, await fs16.readFile(spec, "utf8")));
11488
+ lines.push(...await writeSpecProjectionForFile(spec, await fs17.readFile(spec, "utf8")));
11363
11489
  }
11364
11490
  }
11365
11491
  if (!projected) {
@@ -11368,7 +11494,7 @@ async function writeSpecMarkdownContext(changeDir, change, contextHash, output)
11368
11494
  lines.push(
11369
11495
  "Full source files remain canonical. If a required heading or scenario is missing here, regenerate the handoff or read the source spec directly. Supporting files (proposal, design, tasks) are referenced by hash only."
11370
11496
  );
11371
- await fs16.writeFile(output, lines.join("\n"));
11497
+ await fs17.writeFile(output, lines.join("\n"));
11372
11498
  }
11373
11499
  async function writeSpecJsonContext(changeDir, change, contextHash, output) {
11374
11500
  const entries = [];
@@ -11377,7 +11503,7 @@ async function writeSpecJsonContext(changeDir, change, contextHash, output) {
11377
11503
  const role = /\/specs\/[^/]+\/spec\.md$/u.test(file) ? "spec" : "supporting";
11378
11504
  entries.push({ path: file, sha256: hashFile2(file), role });
11379
11505
  }
11380
- await fs16.writeFile(
11506
+ await fs17.writeFile(
11381
11507
  output,
11382
11508
  `${JSON.stringify(
11383
11509
  {
@@ -11396,8 +11522,8 @@ async function writeSpecJsonContext(changeDir, change, contextHash, output) {
11396
11522
  );
11397
11523
  }
11398
11524
  async function readField2(changeDir, field2) {
11399
- const file = path17.join(changeDir, ".comet.yaml");
11400
- const document = (0, import_yaml6.parseDocument)(await fs16.readFile(file, "utf8"), { uniqueKeys: false });
11525
+ const file = path18.join(changeDir, ".comet.yaml");
11526
+ const document = (0, import_yaml7.parseDocument)(await fs17.readFile(file, "utf8"), { uniqueKeys: false });
11401
11527
  if (document.errors.length > 0) {
11402
11528
  throw new HandoffFailure(`ERROR: Invalid .comet.yaml: ${document.errors[0].message}`);
11403
11529
  }
@@ -11431,7 +11557,7 @@ async function completedHandoffIsCurrent(changeDir, run, contextHash, contextJso
11431
11557
  readCheckpoint(changeDir, run.checkpointRef)
11432
11558
  ]);
11433
11559
  if (!await exists5(contextJson) || !await exists5(contextMd)) return false;
11434
- if (context !== await fs16.readFile(contextMd, "utf8")) return false;
11560
+ if (context !== await fs17.readFile(contextMd, "utf8")) return false;
11435
11561
  if (artifacts.handoff_context !== contextJson || artifacts.handoff_markdown !== contextMd) {
11436
11562
  return false;
11437
11563
  }
@@ -11554,7 +11680,7 @@ var classicHandoffCommand = async (args) => {
11554
11680
  run: pendingRun,
11555
11681
  unknownKeys: (await readClassicState(changeDir)).unknownKeys
11556
11682
  });
11557
- await fs16.mkdir(handoffDir, { recursive: true });
11683
+ await fs17.mkdir(handoffDir, { recursive: true });
11558
11684
  if (handoffMode === "beta") {
11559
11685
  await writeSpecMarkdownContext(changeDir, change, contextHash, contextMd);
11560
11686
  await writeSpecJsonContext(changeDir, change, contextHash, contextJson);
@@ -11562,7 +11688,7 @@ var classicHandoffCommand = async (args) => {
11562
11688
  await writeMarkdownContext(changeDir, change, handoffMode, contextHash, contextMd);
11563
11689
  await writeJsonContext(changeDir, change, handoffMode, contextHash, contextJson);
11564
11690
  }
11565
- const context = await fs16.readFile(contextMd, "utf8");
11691
+ const context = await fs17.readFile(contextMd, "utf8");
11566
11692
  await writeContext(changeDir, pendingRun.contextRef, context);
11567
11693
  const artifacts = {
11568
11694
  ...await readArtifacts(changeDir, pendingRun.artifactsRef),
@@ -11619,37 +11745,24 @@ var classicHandoffCommand = async (args) => {
11619
11745
  };
11620
11746
 
11621
11747
  // domains/comet-classic/classic-hook-guard.ts
11622
- import { existsSync as existsSync2, promises as fs18, readFileSync as readFileSync3 } from "fs";
11623
- import path19 from "path";
11748
+ import { existsSync as existsSync2, promises as fs19, readFileSync as readFileSync3 } from "fs";
11749
+ import path20 from "path";
11624
11750
 
11625
11751
  // domains/comet-classic/classic-current-change.ts
11626
- import { execFileSync } from "child_process";
11627
- import { randomUUID as randomUUID6 } from "crypto";
11628
- import { promises as fs17 } from "fs";
11629
- import path18 from "path";
11752
+ import { randomUUID as randomUUID7 } from "crypto";
11753
+ import { promises as fs18 } from "fs";
11754
+ import path19 from "path";
11630
11755
  function currentChangeFile(projectRoot2) {
11631
- return path18.join(projectRoot2, ".comet", "current-change.json");
11632
- }
11633
- function currentBranch(projectRoot2) {
11634
- try {
11635
- const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
11636
- cwd: projectRoot2,
11637
- encoding: "utf8",
11638
- stdio: ["ignore", "pipe", "ignore"]
11639
- }).trim();
11640
- return branch && branch !== "HEAD" ? branch : null;
11641
- } catch {
11642
- return null;
11643
- }
11756
+ return path19.join(projectRoot2, ".comet", "current-change.json");
11644
11757
  }
11645
11758
  function changeDirectory(projectRoot2, changeName) {
11646
- return path18.join(projectRoot2, "openspec", "changes", changeName);
11759
+ return path19.join(projectRoot2, "openspec", "changes", changeName);
11647
11760
  }
11648
11761
  async function validateActiveChange(projectRoot2, changeName) {
11649
11762
  assertOpenSpecChangeName(changeName);
11650
11763
  const changeDir = changeDirectory(projectRoot2, changeName);
11651
11764
  try {
11652
- await fs17.access(path18.join(changeDir, ".comet.yaml"));
11765
+ await fs18.access(path19.join(changeDir, ".comet.yaml"));
11653
11766
  } catch (error) {
11654
11767
  if (error.code === "ENOENT") {
11655
11768
  throw new Error(
@@ -11690,30 +11803,40 @@ function parseSelection(source) {
11690
11803
  throw new Error("current change selection change must be a string");
11691
11804
  }
11692
11805
  assertOpenSpecChangeName(record.change);
11693
- if (record.branch !== null && typeof record.branch !== "string") {
11806
+ if (record.branch !== void 0 && record.branch !== null && typeof record.branch !== "string") {
11694
11807
  throw new Error("current change selection branch must be a string or null");
11695
11808
  }
11696
11809
  return {
11697
11810
  version: 1,
11698
11811
  change: record.change,
11699
- branch: record.branch
11812
+ branch: record.branch ?? null
11700
11813
  };
11701
11814
  }
11702
11815
  async function selectCurrentChange(projectRoot2, changeName) {
11703
11816
  await validateActiveChange(projectRoot2, changeName);
11817
+ const outcome = await resolveBranchBinding(changeDirectory(projectRoot2, changeName), {
11818
+ heal: true,
11819
+ cwd: projectRoot2
11820
+ });
11821
+ if (outcome.status === "drift") {
11822
+ throw new Error(driftStaleReason(changeName, outcome.boundBranch, outcome.currentBranch));
11823
+ }
11824
+ if (outcome.status === "unbound-detached") {
11825
+ throw new Error(unboundDetachedMessage(changeName));
11826
+ }
11704
11827
  const selection = {
11705
11828
  version: 1,
11706
11829
  change: changeName,
11707
- branch: currentBranch(projectRoot2)
11830
+ branch: outcome.currentBranch
11708
11831
  };
11709
11832
  const file = currentChangeFile(projectRoot2);
11710
- const temporary = `${file}.${randomUUID6()}.tmp`;
11711
- await fs17.mkdir(path18.dirname(file), { recursive: true });
11833
+ const temporary = `${file}.${randomUUID7()}.tmp`;
11834
+ await fs18.mkdir(path19.dirname(file), { recursive: true });
11712
11835
  try {
11713
- await fs17.writeFile(temporary, JSON.stringify(selection, null, 2) + "\n", "utf8");
11714
- await fs17.rename(temporary, file);
11836
+ await fs18.writeFile(temporary, JSON.stringify(selection, null, 2) + "\n", "utf8");
11837
+ await fs18.rename(temporary, file);
11715
11838
  } catch (error) {
11716
- await fs17.rm(temporary, { force: true });
11839
+ await fs18.rm(temporary, { force: true });
11717
11840
  throw error;
11718
11841
  }
11719
11842
  return selection;
@@ -11721,7 +11844,7 @@ async function selectCurrentChange(projectRoot2, changeName) {
11721
11844
  async function resolveCurrentChange(projectRoot2) {
11722
11845
  let source;
11723
11846
  try {
11724
- source = await fs17.readFile(currentChangeFile(projectRoot2), "utf8");
11847
+ source = await fs18.readFile(currentChangeFile(projectRoot2), "utf8");
11725
11848
  } catch (error) {
11726
11849
  if (error.code === "ENOENT") return { status: "missing" };
11727
11850
  return {
@@ -11739,17 +11862,32 @@ async function resolveCurrentChange(projectRoot2) {
11739
11862
  reason: error instanceof Error ? error.message : String(error)
11740
11863
  };
11741
11864
  }
11742
- const branch = currentBranch(projectRoot2);
11743
- if (selection.branch !== null && branch !== selection.branch) {
11865
+ const outcome = await resolveBranchBinding(changeDirectory(projectRoot2, selection.change), {
11866
+ heal: false,
11867
+ cwd: projectRoot2
11868
+ });
11869
+ if (outcome.status === "drift") {
11870
+ return {
11871
+ status: "stale",
11872
+ reason: driftStaleReason(selection.change, outcome.boundBranch, outcome.currentBranch)
11873
+ };
11874
+ }
11875
+ if (outcome.status === "unbound-detached") {
11876
+ return { status: "stale", reason: unboundDetachedMessage(selection.change) };
11877
+ }
11878
+ if (outcome.status === "ok") {
11879
+ return { status: "selected", selection };
11880
+ }
11881
+ if (selection.branch !== null && outcome.currentBranch !== selection.branch) {
11744
11882
  return {
11745
11883
  status: "stale",
11746
- reason: `current change '${selection.change}' was selected on branch '${selection.branch}', current branch is '${branch ?? "detached HEAD"}'`
11884
+ reason: `current change '${selection.change}' was selected on branch '${selection.branch}', current branch is '${outcome.currentBranch ?? "detached HEAD"}'`
11747
11885
  };
11748
11886
  }
11749
11887
  return { status: "selected", selection };
11750
11888
  }
11751
11889
  async function clearCurrentChange(projectRoot2) {
11752
- await fs17.rm(currentChangeFile(projectRoot2), { force: true });
11890
+ await fs18.rm(currentChangeFile(projectRoot2), { force: true });
11753
11891
  }
11754
11892
 
11755
11893
  // domains/comet-classic/classic-hook-guard.ts
@@ -11781,52 +11919,52 @@ function comparisonKey(value) {
11781
11919
  function parseProjectRoot(args) {
11782
11920
  const index = args.indexOf("--project-root");
11783
11921
  const value = index >= 0 ? args[index + 1] : void 0;
11784
- return path19.resolve(value && !value.startsWith("--") ? value : process.cwd());
11922
+ return path20.resolve(value && !value.startsWith("--") ? value : process.cwd());
11785
11923
  }
11786
11924
  function relativeToProjectRoot(target, projectRoot2) {
11787
- const relative = normalized(path19.relative(projectRoot2, target));
11925
+ const relative = normalized(path20.relative(projectRoot2, target));
11788
11926
  if (relative === "") return "";
11789
- if (relative.startsWith("../") || relative === ".." || path19.isAbsolute(relative)) return null;
11927
+ if (relative.startsWith("../") || relative === ".." || path20.isAbsolute(relative)) return null;
11790
11928
  return relative;
11791
11929
  }
11792
11930
  async function physicalPathForPossiblyMissingTarget(target) {
11793
- const resolved = path19.resolve(target);
11794
- const root = path19.parse(resolved).root;
11931
+ const resolved = path20.resolve(target);
11932
+ const root = path20.parse(resolved).root;
11795
11933
  const missingSegments = [];
11796
11934
  let cursor = resolved;
11797
11935
  while (cursor && cursor !== root) {
11798
11936
  try {
11799
- const physicalBase = await fs18.realpath(cursor);
11800
- return path19.join(physicalBase, ...missingSegments.reverse());
11937
+ const physicalBase = await fs19.realpath(cursor);
11938
+ return path20.join(physicalBase, ...missingSegments.reverse());
11801
11939
  } catch (error) {
11802
11940
  const code = error.code;
11803
11941
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
11804
- missingSegments.push(path19.basename(cursor));
11805
- cursor = path19.dirname(cursor);
11942
+ missingSegments.push(path20.basename(cursor));
11943
+ cursor = path20.dirname(cursor);
11806
11944
  }
11807
11945
  }
11808
11946
  try {
11809
- const physicalRoot = await fs18.realpath(root);
11810
- return path19.join(physicalRoot, ...missingSegments.reverse());
11947
+ const physicalRoot = await fs19.realpath(root);
11948
+ return path20.join(physicalRoot, ...missingSegments.reverse());
11811
11949
  } catch {
11812
11950
  return null;
11813
11951
  }
11814
11952
  }
11815
11953
  async function projectRelative(target, projectRoot2) {
11816
- const rawCandidate = path19.isAbsolute(target) ? target : path19.resolve(process.cwd(), target);
11954
+ const rawCandidate = path20.isAbsolute(target) ? target : path20.resolve(process.cwd(), target);
11817
11955
  let candidate = normalized(rawCandidate);
11818
11956
  const rootRelative = relativeToProjectRoot(rawCandidate, projectRoot2);
11819
11957
  if (rootRelative !== null) return rootRelative;
11820
11958
  try {
11821
11959
  const physicalCandidate = await physicalPathForPossiblyMissingTarget(rawCandidate);
11822
- const physicalRoot = await fs18.realpath(projectRoot2);
11960
+ const physicalRoot = await fs19.realpath(projectRoot2);
11823
11961
  if (physicalCandidate) {
11824
11962
  const physicalRootRelative = relativeToProjectRoot(physicalCandidate, physicalRoot);
11825
11963
  if (physicalRootRelative !== null) return physicalRootRelative;
11826
11964
  candidate = normalized(physicalCandidate);
11827
11965
  }
11828
11966
  } catch {
11829
- if (!path19.isAbsolute(target)) return normalized(target).replace(/^\.\//u, "");
11967
+ if (!path20.isAbsolute(target)) return normalized(target).replace(/^\.\//u, "");
11830
11968
  }
11831
11969
  return candidate.replace(/^\.\//u, "");
11832
11970
  }
@@ -11856,15 +11994,15 @@ async function loadGoverningChange(changeDir) {
11856
11994
  }
11857
11995
  }
11858
11996
  async function activeChanges(projectRoot2) {
11859
- const changesDir = path19.join(projectRoot2, "openspec", "changes");
11997
+ const changesDir = path20.join(projectRoot2, "openspec", "changes");
11860
11998
  const governingChanges = [];
11861
11999
  if (!existsSync2(changesDir)) return governingChanges;
11862
- for (const entry2 of (await fs18.readdir(changesDir, { withFileTypes: true })).sort(
12000
+ for (const entry2 of (await fs19.readdir(changesDir, { withFileTypes: true })).sort(
11863
12001
  (left, right) => left.name.localeCompare(right.name)
11864
12002
  )) {
11865
12003
  if (!entry2.isDirectory() || entry2.name === "archive") continue;
11866
- const changeDir = path19.join(changesDir, entry2.name);
11867
- if (!existsSync2(path19.join(changeDir, ".comet.yaml"))) continue;
12004
+ const changeDir = path20.join(changesDir, entry2.name);
12005
+ if (!existsSync2(path20.join(changeDir, ".comet.yaml"))) continue;
11868
12006
  const governing = await loadGoverningChange(changeDir);
11869
12007
  if (!governing || governing.archived) continue;
11870
12008
  governingChanges.push(governing);
@@ -11912,7 +12050,7 @@ function allowsSuperpowersArtifacts(governing) {
11912
12050
  return governing.phase === "design" || governing.phase === "build" || governing.phase === "verify";
11913
12051
  }
11914
12052
  function governingChangeName(governing) {
11915
- return governing.changeDir ? path19.basename(governing.changeDir) : null;
12053
+ return governing.changeDir ? path20.basename(governing.changeDir) : null;
11916
12054
  }
11917
12055
  var SUPERPOWERS_ARTIFACT_SUFFIXES = /* @__PURE__ */ new Set([
11918
12056
  "design",
@@ -11978,7 +12116,30 @@ async function repoSourceGoverningChange(projectRoot2, relativePath2) {
11978
12116
  )
11979
12117
  };
11980
12118
  }
11981
- if (active.length === 1) return active[0];
12119
+ if (active.length === 1) {
12120
+ const sole = active[0];
12121
+ if (sole.changeDir !== null) {
12122
+ const outcome = await resolveBranchBinding(sole.changeDir, {
12123
+ heal: false,
12124
+ cwd: projectRoot2
12125
+ });
12126
+ const name = governingChangeName(sole) ?? "unknown";
12127
+ if (outcome.status === "drift") {
12128
+ return {
12129
+ blockedResult: blockedStaleSelection(
12130
+ relativePath2,
12131
+ driftStaleReason(name, outcome.boundBranch, outcome.currentBranch)
12132
+ )
12133
+ };
12134
+ }
12135
+ if (outcome.status === "unbound-detached") {
12136
+ return {
12137
+ blockedResult: blockedStaleSelection(relativePath2, unboundDetachedMessage(name))
12138
+ };
12139
+ }
12140
+ }
12141
+ return sole;
12142
+ }
11982
12143
  return {
11983
12144
  blockedResult: blockedMultipleChanges(
11984
12145
  relativePath2,
@@ -11992,8 +12153,8 @@ async function governingChange(relativePath2, projectRoot2) {
11992
12153
  const rest = relativePath2.slice(prefix.length);
11993
12154
  const [name] = rest.split("/");
11994
12155
  if (name && name !== "archive") {
11995
- const changeDir = path19.join(projectRoot2, "openspec", "changes", name);
11996
- const stateFile2 = path19.join(changeDir, ".comet.yaml");
12156
+ const changeDir = path20.join(projectRoot2, "openspec", "changes", name);
12157
+ const stateFile2 = path20.join(changeDir, ".comet.yaml");
11997
12158
  if (existsSync2(stateFile2)) {
11998
12159
  const governing = await loadGoverningChange(changeDir);
11999
12160
  if (governing) return governing;
@@ -12601,8 +12762,8 @@ var classicIntentCommand = async (args, _options) => {
12601
12762
  };
12602
12763
 
12603
12764
  // domains/comet-classic/classic-resume-probe.ts
12604
- import path20 from "path";
12605
- import { promises as fs19 } from "fs";
12765
+ import path21 from "path";
12766
+ import { promises as fs20 } from "fs";
12606
12767
  import { spawn } from "child_process";
12607
12768
  var COMET_RESUME_PROBE_SCHEMA_VERSION = "comet.resume_probe.v1";
12608
12769
  function isRecord2(value) {
@@ -12645,13 +12806,13 @@ function result3(action, change, confidence, reason, evidence = []) {
12645
12806
  }
12646
12807
  async function readIfExists(filePath) {
12647
12808
  if (!await fileExists3(filePath)) return "";
12648
- return fs19.readFile(filePath, "utf8");
12809
+ return fs20.readFile(filePath, "utf8");
12649
12810
  }
12650
12811
  async function changeSearchText(changeDir, classic) {
12651
12812
  const files = ["proposal.md", "design.md", "tasks.md"];
12652
12813
  const parts = [classic.name, classic.workflow, classic.phase];
12653
12814
  for (const file of files) {
12654
- parts.push(await readIfExists(path20.join(changeDir, file)));
12815
+ parts.push(await readIfExists(path21.join(changeDir, file)));
12655
12816
  }
12656
12817
  return parts.join("\n").toLowerCase();
12657
12818
  }
@@ -12715,19 +12876,19 @@ function diagnosticFromProjection(changeDir, name, projection) {
12715
12876
  };
12716
12877
  }
12717
12878
  async function hasOpenSpecChangeFiles(changeDir) {
12718
- return await fileExists3(path20.join(changeDir, "proposal.md")) || await fileExists3(path20.join(changeDir, "design.md")) || await fileExists3(path20.join(changeDir, "tasks.md"));
12879
+ return await fileExists3(path21.join(changeDir, "proposal.md")) || await fileExists3(path21.join(changeDir, "design.md")) || await fileExists3(path21.join(changeDir, "tasks.md"));
12719
12880
  }
12720
12881
  async function discoverActiveChanges(projectRoot2) {
12721
- const changesDir = path20.join(projectRoot2, "openspec", "changes");
12882
+ const changesDir = path21.join(projectRoot2, "openspec", "changes");
12722
12883
  if (!await fileExists3(changesDir)) return [];
12723
12884
  const entries = await readDir(changesDir);
12724
12885
  const changes = [];
12725
12886
  for (const entry2 of entries) {
12726
12887
  if (entry2 === "archive") continue;
12727
- const changeDir = path20.join(changesDir, entry2);
12728
- const stat = await fs19.stat(changeDir).catch(() => null);
12888
+ const changeDir = path21.join(changesDir, entry2);
12889
+ const stat = await fs20.stat(changeDir).catch(() => null);
12729
12890
  if (!stat?.isDirectory()) continue;
12730
- const hasCometState = await fileExists3(path20.join(changeDir, ".comet.yaml"));
12891
+ const hasCometState = await fileExists3(path21.join(changeDir, ".comet.yaml"));
12731
12892
  if (!hasCometState) {
12732
12893
  if (!await hasOpenSpecChangeFiles(changeDir)) continue;
12733
12894
  const missingStateChange = {
@@ -13027,11 +13188,11 @@ var classicResumeProbeCommand = async (args) => {
13027
13188
  };
13028
13189
 
13029
13190
  // domains/comet-classic/classic-state-command.ts
13030
- var import_yaml7 = __toESM(require_dist(), 1);
13191
+ var import_yaml8 = __toESM(require_dist(), 1);
13031
13192
  import { spawnSync as spawnSync3 } from "child_process";
13032
- import { randomUUID as randomUUID7 } from "crypto";
13033
- import { existsSync as existsSync3, promises as fs20 } from "fs";
13034
- import path21 from "path";
13193
+ import { randomUUID as randomUUID8 } from "crypto";
13194
+ import { existsSync as existsSync3, promises as fs21 } from "fs";
13195
+ import path22 from "path";
13035
13196
  init_state();
13036
13197
  var GREEN5 = "\x1B[32m";
13037
13198
  var RED5 = "\x1B[31m";
@@ -13046,7 +13207,8 @@ var MACHINE_OWNED_FIELDS = /* @__PURE__ */ new Set([
13046
13207
  "archive_confirmation",
13047
13208
  "verify_failures",
13048
13209
  "classic_profile",
13049
- "classic_migration"
13210
+ "classic_migration",
13211
+ "bound_branch"
13050
13212
  ]);
13051
13213
  var SETTABLE_FIELDS = new Set(
13052
13214
  CLASSIC_WIRE_KEYS.filter((field2) => !MACHINE_OWNED_FIELDS.has(field2))
@@ -13144,7 +13306,7 @@ function validateRelativePath(value, field2) {
13144
13306
  }
13145
13307
  async function exists6(file) {
13146
13308
  try {
13147
- await fs20.access(file);
13309
+ await fs21.access(file);
13148
13310
  return true;
13149
13311
  } catch (error) {
13150
13312
  if (error.code === "ENOENT") return false;
@@ -13153,7 +13315,7 @@ async function exists6(file) {
13153
13315
  }
13154
13316
  async function nonempty3(file) {
13155
13317
  try {
13156
- return (await fs20.stat(file)).size > 0;
13318
+ return (await fs21.stat(file)).size > 0;
13157
13319
  } catch (error) {
13158
13320
  if (error.code === "ENOENT") return false;
13159
13321
  throw error;
@@ -13165,27 +13327,27 @@ async function changeDirectory2(name) {
13165
13327
  async function readDocument2(file) {
13166
13328
  let source;
13167
13329
  try {
13168
- source = await fs20.readFile(file, "utf8");
13330
+ source = await fs21.readFile(file, "utf8");
13169
13331
  } catch (error) {
13170
13332
  if (error.code === "ENOENT") {
13171
13333
  fail2(
13172
- `ERROR: .comet.yaml not found at ${path21.relative(process.cwd(), file).replaceAll("\\", "/")}`
13334
+ `ERROR: .comet.yaml not found at ${path22.relative(process.cwd(), file).replaceAll("\\", "/")}`
13173
13335
  );
13174
13336
  }
13175
13337
  throw error;
13176
13338
  }
13177
- const document = (0, import_yaml7.parseDocument)(source, { uniqueKeys: false });
13339
+ const document = (0, import_yaml8.parseDocument)(source, { uniqueKeys: false });
13178
13340
  if (document.errors.length > 0) fail2(`ERROR: Invalid .comet.yaml: ${document.errors[0].message}`);
13179
13341
  return document;
13180
13342
  }
13181
13343
  async function atomicWrite2(file, content) {
13182
- await fs20.mkdir(path21.dirname(file), { recursive: true });
13183
- const temporary = `${file}.${randomUUID7()}.tmp`;
13344
+ await fs21.mkdir(path22.dirname(file), { recursive: true });
13345
+ const temporary = `${file}.${randomUUID8()}.tmp`;
13184
13346
  try {
13185
- await fs20.writeFile(temporary, content, "utf8");
13186
- await fs20.rename(temporary, file);
13347
+ await fs21.writeFile(temporary, content, "utf8");
13348
+ await fs21.rename(temporary, file);
13187
13349
  } catch (error) {
13188
- await fs20.rm(temporary, { force: true });
13350
+ await fs21.rm(temporary, { force: true });
13189
13351
  throw error;
13190
13352
  }
13191
13353
  }
@@ -13255,6 +13417,7 @@ function sparseClassicState(record) {
13255
13417
  ["current", "branch", "worktree"],
13256
13418
  null
13257
13419
  ),
13420
+ boundBranch: nullableRecordString(record, "bound_branch"),
13258
13421
  verifyMode: enumRecordValue(record, "verify_mode", ["light", "full"], null),
13259
13422
  autoTransition: nullableRecordBoolean(record, "auto_transition"),
13260
13423
  baseRef: nullableRecordString(record, "base_ref"),
@@ -13327,7 +13490,7 @@ async function stateFile(name) {
13327
13490
  const change = await changeDirectory2(name);
13328
13491
  return {
13329
13492
  ...change,
13330
- file: path21.join(change.directory, ".comet.yaml")
13493
+ file: path22.join(change.directory, ".comet.yaml")
13331
13494
  };
13332
13495
  }
13333
13496
  async function readField3(name, field2) {
@@ -13345,7 +13508,7 @@ async function readField3(name, field2) {
13345
13508
  return scalar(value);
13346
13509
  }
13347
13510
  function parsedValue(field2, value) {
13348
- const document = (0, import_yaml7.parseDocument)(`${field2}: ${value}
13511
+ const document = (0, import_yaml8.parseDocument)(`${field2}: ${value}
13349
13512
  `);
13350
13513
  if (document.errors.length > 0) fail2(`ERROR: Invalid value: '${value}'`);
13351
13514
  return document.get(field2);
@@ -13380,7 +13543,35 @@ async function setField2(output, name, field2, value, options = {}) {
13380
13543
  validateSetValue(field2, value);
13381
13544
  const { file, directory } = await stateFile(name);
13382
13545
  const document = await readDocument2(file);
13546
+ const previousRecord = document.toJS() ?? {};
13383
13547
  document.set(field2, parsedValue(field2, value));
13548
+ if (field2 === "isolation") {
13549
+ if (requiresBranchBinding(value)) {
13550
+ const previousIsolation = typeof previousRecord.isolation === "string" ? previousRecord.isolation : null;
13551
+ const existing = previousRecord.bound_branch;
13552
+ const alreadyBound = typeof existing === "string" && existing !== "";
13553
+ if (!alreadyBound || previousIsolation !== value) {
13554
+ const currentBranch = liveGitBranch(process.cwd());
13555
+ const verdict = evaluateBranchBinding({
13556
+ isolation: value,
13557
+ boundBranch: null,
13558
+ currentBranch,
13559
+ gitWorkTree: currentBranch === null ? isGitWorkTree(process.cwd()) : true
13560
+ });
13561
+ if (verdict.status === "needs-heal") {
13562
+ document.set("bound_branch", verdict.branch);
13563
+ } else if (verdict.status === "unbound-detached") {
13564
+ fail2(
13565
+ `ERROR: cannot bind isolation=${value} while HEAD is detached; checkout a branch first`
13566
+ );
13567
+ } else {
13568
+ document.set("bound_branch", null);
13569
+ }
13570
+ }
13571
+ } else {
13572
+ document.set("bound_branch", null);
13573
+ }
13574
+ }
13384
13575
  const run = await readRunState(directory);
13385
13576
  const projection = parseClassicStateDocument(document.toJS(), run);
13386
13577
  if (projection.run) {
@@ -13430,10 +13621,10 @@ async function init(output, name, workflow) {
13430
13621
  validateEnum(workflow, PROFILES);
13431
13622
  const { file, label, directory } = await stateFile(name);
13432
13623
  if (await exists6(file)) fail2(`ERROR: .comet.yaml already exists at ${label}/.comet.yaml`);
13433
- await fs20.mkdir(directory, { recursive: true });
13624
+ await fs21.mkdir(directory, { recursive: true });
13434
13625
  const preset = workflow !== "full";
13435
13626
  const reviewMode = preset ? "off" : await reviewModeDefault();
13436
- const document = new import_yaml7.Document({
13627
+ const document = new import_yaml8.Document({
13437
13628
  workflow,
13438
13629
  language: await projectLanguageDefault(),
13439
13630
  phase: "open",
@@ -13443,7 +13634,7 @@ async function init(output, name, workflow) {
13443
13634
  subagent_dispatch: null,
13444
13635
  tdd_mode: preset ? "direct" : null,
13445
13636
  review_mode: reviewMode,
13446
- isolation: preset ? "current" : null,
13637
+ isolation: null,
13447
13638
  verify_mode: preset ? "light" : null,
13448
13639
  auto_transition: await autoTransition() === "true",
13449
13640
  base_ref: gitOutput(["rev-parse", "--verify", "HEAD"]),
@@ -13475,10 +13666,10 @@ async function requireBuildDecisions(name) {
13475
13666
  const subagentDispatch = await readField3(name, "subagent_dispatch");
13476
13667
  const tddMode = await readField3(name, "tdd_mode");
13477
13668
  const reviewMode = await readField3(name, "review_mode");
13478
- const allowedIsolation = workflow === "full" ? ["branch", "worktree"] : ["current", "branch", "worktree"];
13669
+ const allowedIsolation = ["current", "branch", "worktree"];
13479
13670
  if (!allowedIsolation.includes(isolation)) {
13480
13671
  fail2(
13481
- `ERROR: Cannot transition '${name}': isolation must be ${workflow === "full" ? "branch or worktree" : "current, branch, or worktree"}, got '${isolation || "null"}'`
13672
+ `ERROR: Cannot transition '${name}': isolation must be current, branch, or worktree, got '${isolation || "null"}'`
13482
13673
  );
13483
13674
  }
13484
13675
  if (!["subagent-driven-development", "executing-plans", "direct"].includes(buildMode)) {
@@ -13511,13 +13702,13 @@ async function requireOpenArtifacts(name) {
13511
13702
  const { directory } = await stateFile(name);
13512
13703
  const workflow = await readField3(name, "workflow");
13513
13704
  for (const artifact of ["proposal.md", "tasks.md"]) {
13514
- if (!await nonempty3(path21.join(directory, artifact))) {
13705
+ if (!await nonempty3(path22.join(directory, artifact))) {
13515
13706
  fail2(
13516
13707
  `ERROR: Cannot transition '${name}': ${artifact} must exist and be non-empty before leaving open`
13517
13708
  );
13518
13709
  }
13519
13710
  }
13520
- if (workflow === "full" && !await nonempty3(path21.join(directory, "design.md"))) {
13711
+ if (workflow === "full" && !await nonempty3(path22.join(directory, "design.md"))) {
13521
13712
  fail2(
13522
13713
  `ERROR: Cannot transition '${name}': design.md must exist and be non-empty before leaving open`
13523
13714
  );
@@ -13525,14 +13716,14 @@ async function requireOpenArtifacts(name) {
13525
13716
  }
13526
13717
  async function requireDesignEvidence(name) {
13527
13718
  const designDoc = await readField3(name, "design_doc");
13528
- if (!designDoc || designDoc === "null" || !await nonempty3(path21.resolve(designDoc))) {
13719
+ if (!designDoc || designDoc === "null" || !await nonempty3(path22.resolve(designDoc))) {
13529
13720
  fail2(
13530
13721
  `ERROR: Cannot transition '${name}': design_doc must point to an existing Design Doc before leaving design`
13531
13722
  );
13532
13723
  }
13533
13724
  }
13534
13725
  async function writeSparseTransitionEffects(directory, effects) {
13535
- const file = path21.join(directory, ".comet.yaml");
13726
+ const file = path22.join(directory, ".comet.yaml");
13536
13727
  const document = await readDocument2(file);
13537
13728
  for (const effect of effects) {
13538
13729
  const field2 = wireField2(effect.field);
@@ -13547,7 +13738,7 @@ async function applyTransitionEvent(output, name, event) {
13547
13738
  let sparse = false;
13548
13739
  if (!classic) {
13549
13740
  if (projection.run) fail2("ERROR: Classic state projection is missing");
13550
- const document = await readDocument2(path21.join(directory, ".comet.yaml"));
13741
+ const document = await readDocument2(path22.join(directory, ".comet.yaml"));
13551
13742
  classic = sparseClassicState(document.toJS());
13552
13743
  sparse = true;
13553
13744
  }
@@ -13594,7 +13785,7 @@ async function transition(output, name, event) {
13594
13785
  } else if (event === "verify-pass") {
13595
13786
  await requirePhase(name, "verify");
13596
13787
  const report = await readField3(name, "verification_report");
13597
- if (!report || !await exists6(path21.resolve(report))) {
13788
+ if (!report || !await exists6(path22.resolve(report))) {
13598
13789
  fail2(
13599
13790
  `ERROR: Cannot transition '${name}': verification_report must point to an existing report file`
13600
13791
  );
@@ -13658,9 +13849,9 @@ async function next(output, name) {
13658
13849
  async function taskCheckoff(output, taskFile, taskText) {
13659
13850
  validateRelativePath(taskFile, "task file");
13660
13851
  if (!taskText) fail2("ERROR: Task text cannot be empty");
13661
- const file = path21.resolve(taskFile);
13852
+ const file = path22.resolve(taskFile);
13662
13853
  if (!await exists6(file)) fail2(`ERROR: Task file not found: ${taskFile}`);
13663
- const lines = (await fs20.readFile(file, "utf8")).split(/\r?\n/u);
13854
+ const lines = (await fs21.readFile(file, "utf8")).split(/\r?\n/u);
13664
13855
  const matches = lines.filter(
13665
13856
  (line) => [`- [ ] ${taskText}`, `- [x] ${taskText}`, `- [X] ${taskText}`].includes(line)
13666
13857
  );
@@ -13698,21 +13889,21 @@ async function check2(output, name, phase) {
13698
13889
  designDoc ? `design_doc=${designDoc} (expected: empty/null)` : "design_doc is empty/null"
13699
13890
  );
13700
13891
  for (const artifact of ["proposal.md", "design.md", "tasks.md"]) {
13701
- (await nonempty3(path21.join(directory, artifact)) ? pass2 : reject)(
13702
- `${artifact} ${await nonempty3(path21.join(directory, artifact)) ? "non-empty" : "missing or empty"}`
13892
+ (await nonempty3(path22.join(directory, artifact)) ? pass2 : reject)(
13893
+ `${artifact} ${await nonempty3(path22.join(directory, artifact)) ? "non-empty" : "missing or empty"}`
13703
13894
  );
13704
13895
  }
13705
13896
  } else if (phase === "build") {
13706
13897
  const workflow = await readField3(name, "workflow");
13707
13898
  const designDoc = await readField3(name, "design_doc");
13708
13899
  if (workflow === "full") {
13709
- (designDoc && designDoc !== "null" && await exists6(path21.resolve(designDoc)) ? pass2 : reject)(`design_doc=${designDoc} (expected: non-null and file exists)`);
13900
+ (designDoc && designDoc !== "null" && await exists6(path22.resolve(designDoc)) ? pass2 : reject)(`design_doc=${designDoc} (expected: non-null and file exists)`);
13710
13901
  } else {
13711
13902
  pass2(`workflow=${workflow} (design_doc not required)`);
13712
13903
  }
13713
13904
  for (const artifact of ["proposal.md", "tasks.md"]) {
13714
- (await nonempty3(path21.join(directory, artifact)) ? pass2 : reject)(
13715
- `${artifact} ${await nonempty3(path21.join(directory, artifact)) ? "non-empty" : "missing or empty"}`
13905
+ (await nonempty3(path22.join(directory, artifact)) ? pass2 : reject)(
13906
+ `${artifact} ${await nonempty3(path22.join(directory, artifact)) ? "non-empty" : "missing or empty"}`
13716
13907
  );
13717
13908
  }
13718
13909
  } else if (phase === "verify") {
@@ -13725,6 +13916,29 @@ async function check2(output, name, phase) {
13725
13916
  const archived = await readField3(name, "archived");
13726
13917
  (archived !== "true" ? pass2 : reject)(`archived=${archived} (expected: not true)`);
13727
13918
  }
13919
+ const binding = await resolveBranchBinding(directory, { heal: true, cwd: process.cwd() });
13920
+ if (binding.bindingRequired) {
13921
+ switch (binding.status) {
13922
+ case "drift":
13923
+ reject(driftBlockedMessage(name, binding.boundBranch, binding.currentBranch));
13924
+ break;
13925
+ case "unbound-detached":
13926
+ reject(unboundDetachedMessage(name));
13927
+ break;
13928
+ case "healed":
13929
+ pass2(`bound_branch lazily set to ${binding.branch}`);
13930
+ break;
13931
+ case "needs-heal":
13932
+ case "ok":
13933
+ case "not-applicable":
13934
+ pass2("bound_branch matches current branch");
13935
+ break;
13936
+ default: {
13937
+ const exhaustive = binding;
13938
+ throw new Error(`unhandled branch binding status: ${JSON.stringify(exhaustive)}`);
13939
+ }
13940
+ }
13941
+ }
13728
13942
  output.stdout.push("");
13729
13943
  if (blocked2) {
13730
13944
  output.stderr.push(red4("BLOCKED — fix failing checks before proceeding"));
@@ -13734,7 +13948,7 @@ async function check2(output, name, phase) {
13734
13948
  }
13735
13949
  function fieldStatus(field2, value, file) {
13736
13950
  if (!value || value === "null") return ` - ${field2}: PENDING`;
13737
- if (file && !existsSync3(path21.resolve(file))) {
13951
+ if (file && !existsSync3(path22.resolve(file))) {
13738
13952
  return ` - ${field2}: BROKEN (path ${value} does not exist)`;
13739
13953
  }
13740
13954
  return ` - ${field2}: DONE (${value})`;
@@ -13743,7 +13957,7 @@ async function recoverOpen(output, directory) {
13743
13957
  output.stdout.push(" Artifacts:");
13744
13958
  let complete = 0;
13745
13959
  for (const artifact of ["proposal.md", "design.md", "tasks.md"]) {
13746
- const done = await nonempty3(path21.join(directory, artifact));
13960
+ const done = await nonempty3(path22.join(directory, artifact));
13747
13961
  if (done) complete += 1;
13748
13962
  output.stdout.push(` - ${artifact}: ${done ? "DONE" : "PENDING"}`);
13749
13963
  }
@@ -13756,7 +13970,7 @@ async function recoverDesign(output, name, directory) {
13756
13970
  output.stdout.push(" Artifacts:");
13757
13971
  for (const artifact of ["proposal.md", "design.md", "tasks.md"]) {
13758
13972
  output.stdout.push(
13759
- ` - ${artifact}: ${await nonempty3(path21.join(directory, artifact)) ? "DONE" : "MISSING (unexpected in design phase)"}`
13973
+ ` - ${artifact}: ${await nonempty3(path22.join(directory, artifact)) ? "DONE" : "MISSING (unexpected in design phase)"}`
13760
13974
  );
13761
13975
  }
13762
13976
  const handoff = await readField3(name, "handoff_context");
@@ -13770,11 +13984,11 @@ async function recoverDesign(output, name, directory) {
13770
13984
  fieldStatus("design_doc", design, design),
13771
13985
  ""
13772
13986
  );
13773
- if (design && design !== "null" && await exists6(path21.resolve(design))) {
13987
+ if (design && design !== "null" && await exists6(path22.resolve(design))) {
13774
13988
  output.stdout.push(
13775
13989
  "Recovery action: Design Doc already created and linked. Run guard to transition to build."
13776
13990
  );
13777
- } else if (handoff && handoff !== "null" && await exists6(path21.resolve(handoff))) {
13991
+ } else if (handoff && handoff !== "null" && await exists6(path22.resolve(handoff))) {
13778
13992
  output.stdout.push(
13779
13993
  "Recovery action: Handoff generated but Design Doc not yet created. Resume from brainstorming confirmation (Step 1c)."
13780
13994
  );
@@ -13804,7 +14018,7 @@ async function recoverBuild(output, name, directory, workflow) {
13804
14018
  decisions.push(fieldStatus("subagent_dispatch", subagentDispatch));
13805
14019
  }
13806
14020
  output.stdout.push(...decisions, "", " Plan:", fieldStatus("plan", plan, plan), "");
13807
- const tasks = path21.join(directory, "tasks.md");
14021
+ const tasks = path22.join(directory, "tasks.md");
13808
14022
  if (!await exists6(tasks)) {
13809
14023
  output.stdout.push(
13810
14024
  " Tasks: tasks.md MISSING",
@@ -13813,14 +14027,14 @@ async function recoverBuild(output, name, directory, workflow) {
13813
14027
  );
13814
14028
  return;
13815
14029
  }
13816
- const lines = (await fs20.readFile(tasks, "utf8")).split(/\r?\n/u);
14030
+ const lines = (await fs21.readFile(tasks, "utf8")).split(/\r?\n/u);
13817
14031
  const total = lines.filter((line) => /^\s*- \[[ xX]\] /u.test(line)).length;
13818
14032
  const done = lines.filter((line) => /^\s*- \[[xX]\] /u.test(line)).length;
13819
14033
  const pending = total - done;
13820
14034
  let planTotal = 0;
13821
14035
  let planDone = 0;
13822
- if (plan && plan !== "null" && await exists6(path21.resolve(plan))) {
13823
- const planLines = (await fs20.readFile(path21.resolve(plan), "utf8")).split(/\r?\n/u);
14036
+ if (plan && plan !== "null" && await exists6(path22.resolve(plan))) {
14037
+ const planLines = (await fs21.readFile(path22.resolve(plan), "utf8")).split(/\r?\n/u);
13824
14038
  planTotal = planLines.filter((line) => /^\s*- \[[ xX]\] /u.test(line)).length;
13825
14039
  planDone = planLines.filter((line) => /^\s*- \[[xX]\] /u.test(line)).length;
13826
14040
  }
@@ -13951,19 +14165,19 @@ async function scale(output, name) {
13951
14165
  validateChangeName4(name);
13952
14166
  const { file, directory, label } = await stateFile(name);
13953
14167
  if (!await exists6(file)) fail2(`ERROR: .comet.yaml not found at ${label}/.comet.yaml`);
13954
- const tasksFile = path21.join(directory, "tasks.md");
13955
- const taskCount = await exists6(tasksFile) ? (await fs20.readFile(tasksFile, "utf8")).split(/\r?\n/u).filter((line) => /^- \[/u.test(line)).length : 0;
13956
- const specs = path21.join(directory, "specs");
14168
+ const tasksFile = path22.join(directory, "tasks.md");
14169
+ const taskCount = await exists6(tasksFile) ? (await fs21.readFile(tasksFile, "utf8")).split(/\r?\n/u).filter((line) => /^- \[/u.test(line)).length : 0;
14170
+ const specs = path22.join(directory, "specs");
13957
14171
  let deltaSpecs = 0;
13958
14172
  if (await exists6(specs)) {
13959
- for (const entry2 of await fs20.readdir(specs)) {
13960
- if (await exists6(path21.join(specs, entry2, "spec.md"))) deltaSpecs += 1;
14173
+ for (const entry2 of await fs21.readdir(specs)) {
14174
+ if (await exists6(path22.join(specs, entry2, "spec.md"))) deltaSpecs += 1;
13961
14175
  }
13962
14176
  }
13963
14177
  const plan = await readField3(name, "plan");
13964
14178
  let baseRef = "";
13965
- if (plan && plan !== "null" && await exists6(path21.resolve(plan))) {
13966
- const match = (await fs20.readFile(path21.resolve(plan), "utf8")).match(/^base-ref:\s*(.+)$/mu);
14179
+ if (plan && plan !== "null" && await exists6(path22.resolve(plan))) {
14180
+ const match = (await fs21.readFile(path22.resolve(plan), "utf8")).match(/^base-ref:\s*(.+)$/mu);
13967
14181
  baseRef = match?.[1].trim() ?? "";
13968
14182
  }
13969
14183
  if (!baseRef) baseRef = await readField3(name, "base_ref");
@@ -14043,15 +14257,42 @@ async function selectChange(output, name) {
14043
14257
  validateChangeName4(name);
14044
14258
  try {
14045
14259
  const selection = await selectCurrentChange(process.cwd(), name);
14260
+ const boundBranch = await readField3(name, "bound_branch");
14261
+ const bound = boundBranch && boundBranch !== "null" ? boundBranch : null;
14046
14262
  output.stderr.push(
14047
- green4(
14048
- `[SELECTED] current change: ${selection.change}${selection.branch ? ` (branch: ${selection.branch})` : ""}`
14049
- )
14263
+ green4(`[SELECTED] current change: ${selection.change}${bound ? ` (branch: ${bound})` : ""}`)
14050
14264
  );
14051
14265
  } catch (error) {
14052
14266
  fail2(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
14053
14267
  }
14054
14268
  }
14269
+ async function rebind(output, name) {
14270
+ validateChangeName4(name);
14271
+ const { directory } = await stateFile(name);
14272
+ const boundBranch = await readField3(name, "bound_branch");
14273
+ if (!boundBranch || boundBranch === "null") {
14274
+ fail2(
14275
+ `ERROR: '${name}' is not yet bound; use 'comet state set ${name} isolation <current|branch|worktree>' to establish the first binding`
14276
+ );
14277
+ }
14278
+ const branch = liveGitBranch(process.cwd());
14279
+ if (branch === null) {
14280
+ fail2("ERROR: cannot rebind while HEAD is detached; checkout a branch first");
14281
+ }
14282
+ const before = await readClassicState(directory);
14283
+ if (!before.classic) fail2("ERROR: Classic state projection is missing");
14284
+ await healBoundBranch(directory, branch);
14285
+ const after = { ...before.classic, boundBranch: branch };
14286
+ await appendClassicStateEvent(directory, {
14287
+ change: name,
14288
+ event: "rebind",
14289
+ source: "comet-state",
14290
+ from: before.classic,
14291
+ to: after,
14292
+ effects: [{ field: "boundBranch", from: boundBranch, to: branch }]
14293
+ });
14294
+ output.stderr.push(green4(`[REBIND] bound_branch: ${boundBranch} → ${branch}`));
14295
+ }
14055
14296
  async function currentChange(output) {
14056
14297
  const resolution = await resolveCurrentChange(process.cwd());
14057
14298
  if (resolution.status === "selected") {
@@ -14105,6 +14346,9 @@ var classicStateCommand = async (args) => {
14105
14346
  } else if (subcommand === "task-checkoff") {
14106
14347
  required(rest, 2, "Usage: comet-state.mjs task-checkoff <file> <task-text>");
14107
14348
  await taskCheckoff(output, rest[0], rest[1]);
14349
+ } else if (subcommand === "rebind") {
14350
+ requiredExact(rest, 1, "Usage: comet-state.mjs rebind <change-name>");
14351
+ await rebind(output, rest[0]);
14108
14352
  } else if (subcommand === "select") {
14109
14353
  requiredExact(rest, 1, "Usage: comet-state.mjs select <change-name>");
14110
14354
  await selectChange(output, rest[0]);