syntaur 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-D_1PqV49.js → _basePickBy-iqEGomW8.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-Go3lJYWk.js → _baseUniq--Qg0UGRL.js} +1 -1
  4. package/dashboard/dist/assets/{arc-CPkf0K8o.js → arc-DARwlKiU.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D-YQc-ni.js → architectureDiagram-2XIMDMQ5-oWU1lL2y.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BYKjgbgS.js → blockDiagram-WCTKOSBZ-DMuCwZLv.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-DL0q4c_7.js → c4Diagram-IC4MRINW-Axpp-L4P.js} +1 -1
  8. package/dashboard/dist/assets/channel-DJkgIua7.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-Dz3hphvh.js → chunk-4BX2VUAB-BE-Yhqdt.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-KkMi7-rO.js → chunk-55IACEB6-DacZdH_M.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-C6i7PgE-.js → chunk-FMBD7UC4-vAEg5Z3N.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-D9nWD3SH.js → chunk-JSJVCQXG-CTukijKn.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-C-pz_QfY.js → chunk-KX2RTZJC-C-lU6Wil.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-m21dA_AB.js → chunk-NQ4KR5QH-m-jZxhEZ.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BTYD7vUi.js → chunk-QZHKN3VN-VA8PtmgY.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-D_92eLJ7.js → chunk-WL4C6EOR-BhM9cEr9.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BQpVKOOw.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BQpVKOOw.js +1 -0
  19. package/dashboard/dist/assets/clone-Cyb0u6Fm.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-xG37nIJ5.js → cose-bilkent-S5V4N54A-DBGtEkKf.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-BmMnqFf5.js → dagre-KLK3FWXG-BIU8RfcY.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-NKwCHorn.js → diagram-E7M64L7V-C1OD19qw.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-RFbM55z9.js → diagram-IFDJBPK2-B-VWSLa6.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DFATQN63.js → diagram-P4PSJMXO-gpFwgRk6.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-CnkbUfYo.js → erDiagram-INFDFZHY-MxL4ZzOD.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-C2bCpv_b.js → flowDiagram-PKNHOUZH-B0BqXx8A.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Cyz3i9ox.js → ganttDiagram-A5KZAMGK-CZSBVo4l.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-DNSJOJPu.js → gitGraphDiagram-K3NZZRJ6-CfXBKQ-d.js} +1 -1
  29. package/dashboard/dist/assets/{graph-B0uvMYEt.js → graph-IrjCiOoy.js} +1 -1
  30. package/dashboard/dist/assets/index-BHyNXpJx.js +577 -0
  31. package/dashboard/dist/assets/index-KhylT_54.css +1 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CIAGOhnc.js → infoDiagram-LFFYTUFH-Rf9ZeCX3.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-Cz1e1z95.js → ishikawaDiagram-PHBUUO56-RAQWTALS.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DQpnaGS3.js → journeyDiagram-4ABVD52K-ZcEWsM-p.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-ctedqNYC.js → kanban-definition-K7BYSVSG-B-GmGguF.js} +1 -1
  36. package/dashboard/dist/assets/{layout-DB-MZUE7.js → layout-CGl1QE2H.js} +1 -1
  37. package/dashboard/dist/assets/{linear-ChyfHemL.js → linear-C8T_EWa_.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-B7D9g8Z0.js → mermaid.core-_rgvEzla.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BBwO3Cx_.js → mindmap-definition-YRQLILUH-D9eiyPrJ.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-D5cgYxWS.js → pieDiagram-SKSYHLDU-DuAS5_1b.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-D1PRaNvQ.js → quadrantDiagram-337W2JSQ-Bs9cFwnM.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-DSPKmecF.js → requirementDiagram-Z7DCOOCP-P8h8bKqs.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-qoBrVPPJ.js → sankeyDiagram-WA2Y5GQK-jvYtbQv9.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-COHgbUzd.js → sequenceDiagram-2WXFIKYE-dfHtIxLr.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D7o2ec5P.js → stateDiagram-RAJIS63D-BVKA9rS_.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-B1fHHK2b.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-0_qvF5FF.js → timeline-definition-YZTLITO2-Cm1H8Att.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-DrWRWtnJ.js → treemap-KZPCXAKY-BRzwg_ce.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Chnf9HfQ.js → vennDiagram-LZ73GAT5-BJ4UoDVK.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DZaMVdVS.js → xychartDiagram-JWTSCODW-TyeURVy0.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +300 -171
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +300 -171
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +35 -21
  57. package/dist/launch/index.js +151 -85
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  61. package/platforms/claude-code/skills/manage-statuses/SKILL.md +17 -0
  62. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  63. package/platforms/codex/skills/manage-statuses/SKILL.md +17 -0
  64. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  65. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  66. package/skills/manage-statuses/SKILL.md +17 -0
  67. package/dashboard/dist/assets/channel-BogPsJQf.js +0 -1
  68. package/dashboard/dist/assets/classDiagram-VBA2DB6C-LFy25hGC.js +0 -1
  69. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-LFy25hGC.js +0 -1
  70. package/dashboard/dist/assets/clone-B2jJrmVo.js +0 -1
  71. package/dashboard/dist/assets/index-J9J1LmFz.js +0 -567
  72. package/dashboard/dist/assets/index-hXP7kHLF.css +0 -1
  73. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Bn85KIR5.js +0 -1
package/dist/index.js CHANGED
@@ -2411,6 +2411,131 @@ var init_slug = __esm({
2411
2411
  }
2412
2412
  });
2413
2413
 
2414
+ // src/utils/derive-config.ts
2415
+ function validateDeriveShape(value) {
2416
+ const problems = [];
2417
+ if (!value || typeof value !== "object") {
2418
+ return ["derive must be an object"];
2419
+ }
2420
+ const d = value;
2421
+ if (!Array.isArray(d.phaseLadder)) {
2422
+ problems.push("derive.phaseLadder must be an array");
2423
+ } else {
2424
+ d.phaseLadder.forEach((rung, i) => {
2425
+ if (!rung || typeof rung !== "object") {
2426
+ problems.push(`derive.phaseLadder[${i}] must be an object`);
2427
+ return;
2428
+ }
2429
+ const r = rung;
2430
+ if (typeof r.phase !== "string") problems.push(`derive.phaseLadder[${i}].phase must be a string`);
2431
+ if (typeof r.when !== "string") problems.push(`derive.phaseLadder[${i}].when must be a string`);
2432
+ if (r.next !== void 0 && typeof r.next !== "string") {
2433
+ problems.push(`derive.phaseLadder[${i}].next must be a string when present`);
2434
+ }
2435
+ });
2436
+ }
2437
+ if (!Array.isArray(d.disposition)) {
2438
+ problems.push("derive.disposition must be an array");
2439
+ } else {
2440
+ d.disposition.forEach((rule, i) => {
2441
+ if (!rule || typeof rule !== "object") {
2442
+ problems.push(`derive.disposition[${i}] must be an object`);
2443
+ return;
2444
+ }
2445
+ const r = rule;
2446
+ if (!(r.when === null || typeof r.when === "string")) {
2447
+ problems.push(`derive.disposition[${i}].when must be a string or null`);
2448
+ }
2449
+ if (typeof r.is !== "string") problems.push(`derive.disposition[${i}].is must be a string`);
2450
+ });
2451
+ }
2452
+ const headline = d.headline;
2453
+ if (!headline || typeof headline !== "object") {
2454
+ problems.push("derive.headline must be an object");
2455
+ } else {
2456
+ if (typeof headline.parked !== "string") problems.push("derive.headline.parked must be a string");
2457
+ if (typeof headline.blocked !== "string") problems.push("derive.headline.blocked must be a string");
2458
+ }
2459
+ return problems;
2460
+ }
2461
+ function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
2462
+ const problems = [];
2463
+ const ids = new Set(statusConfig.statuses.map((s) => s.id));
2464
+ if (derive.phaseLadder.length === 0) {
2465
+ problems.push("phaseLadder must have at least one rung");
2466
+ }
2467
+ for (const rung of derive.phaseLadder) {
2468
+ if (!ids.has(rung.phase)) {
2469
+ problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
2470
+ }
2471
+ const err2 = rung.when === "*" ? null : validateWhen(rung.when);
2472
+ if (err2) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err2}`);
2473
+ }
2474
+ const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
2475
+ for (const rule of derive.disposition) {
2476
+ if (!VALID_DISPOSITIONS.has(rule.is)) {
2477
+ problems.push(
2478
+ `disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
2479
+ );
2480
+ }
2481
+ if (rule.when !== null) {
2482
+ const err2 = validateWhen(rule.when);
2483
+ if (err2) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err2}`);
2484
+ }
2485
+ }
2486
+ const elseIndices = derive.disposition.map((r, i) => r.when === null ? i : -1).filter((i) => i >= 0);
2487
+ if (elseIndices.length === 0) {
2488
+ problems.push("disposition rules must end with an `else:` arm (a rule with when: null)");
2489
+ } else if (elseIndices.length > 1) {
2490
+ problems.push("disposition rules must have exactly one `else:` arm (when: null)");
2491
+ } else if (elseIndices[0] !== derive.disposition.length - 1) {
2492
+ problems.push("the `else:` arm (when: null) must be the LAST disposition rule \u2014 rules after it are unreachable");
2493
+ }
2494
+ for (const key of ["parked", "blocked"]) {
2495
+ if (!ids.has(derive.headline[key])) {
2496
+ problems.push(
2497
+ `headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
2498
+ );
2499
+ }
2500
+ }
2501
+ return problems;
2502
+ }
2503
+ var DEFAULT_DERIVE_CONFIG;
2504
+ var init_derive_config = __esm({
2505
+ "src/utils/derive-config.ts"() {
2506
+ "use strict";
2507
+ DEFAULT_DERIVE_CONFIG = {
2508
+ phaseLadder: [
2509
+ { phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
2510
+ {
2511
+ // planExists-but-not-approved also sits here: the default status set has
2512
+ // no `planning` id. Users who define one add a `planExists:true` rung.
2513
+ phase: "ready_for_planning",
2514
+ when: "hasRealObjective:true AND acRealTotal > 0",
2515
+ next: "Write a plan and get it approved"
2516
+ },
2517
+ { phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
2518
+ {
2519
+ phase: "in_progress",
2520
+ when: "planApproved:true AND implementationStarted:true",
2521
+ next: "Finish acceptance criteria, then request review"
2522
+ },
2523
+ {
2524
+ phase: "review",
2525
+ when: "acAllChecked:true OR reviewRequested:true",
2526
+ next: "Complete, or address review feedback"
2527
+ }
2528
+ ],
2529
+ disposition: [
2530
+ { when: "parked:true", is: "parked" },
2531
+ { when: "blocked:true", is: "blocked" },
2532
+ { when: null, is: "active" }
2533
+ ],
2534
+ headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
2535
+ };
2536
+ }
2537
+ });
2538
+
2414
2539
  // src/utils/query/fields.ts
2415
2540
  function resolveField(registry, name) {
2416
2541
  return registry[name.toLowerCase()] ?? null;
@@ -3107,6 +3232,24 @@ function factFieldNames(decl) {
3107
3232
  ].map((k) => k.toLowerCase()) : [exportNames.fact.toLowerCase()];
3108
3233
  return { storageKey: name, exports: exportNames, registryKeys };
3109
3234
  }
3235
+ function normalizeFactDeclarations(raw2) {
3236
+ const out = [];
3237
+ for (const row of raw2 ?? []) {
3238
+ if (!row || typeof row.name !== "string") continue;
3239
+ const name = row.name.trim();
3240
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) continue;
3241
+ const type = (row.type ?? "").trim();
3242
+ if (type === "bool" || type === "number") {
3243
+ out.push({ name, type });
3244
+ } else if (type === "attestation") {
3245
+ const binds = (row.binds ?? "none").toString().trim() || "none";
3246
+ if (binds === "plan" || binds === "commit" || binds === "none") {
3247
+ out.push({ name, type: "attestation", binds });
3248
+ }
3249
+ }
3250
+ }
3251
+ return out;
3252
+ }
3110
3253
  function validateFactDeclarations(raw2) {
3111
3254
  const problems = [];
3112
3255
  const owners = /* @__PURE__ */ new Map();
@@ -3356,6 +3499,7 @@ __export(config_exports, {
3356
3499
  updatePlaybooksConfig: () => updatePlaybooksConfig,
3357
3500
  validateAgentList: () => validateAgentList,
3358
3501
  validateDeriveConfig: () => validateDeriveConfig,
3502
+ validateDeriveShape: () => validateDeriveShape,
3359
3503
  validateFactDeclarations: () => validateFactDeclarations,
3360
3504
  writeAgentsConfig: () => writeAgentsConfig,
3361
3505
  writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
@@ -3367,24 +3511,6 @@ __export(config_exports, {
3367
3511
  import { readFile as readFile5 } from "fs/promises";
3368
3512
  import { spawnSync } from "child_process";
3369
3513
  import { resolve as resolve6, isAbsolute } from "path";
3370
- function normalizeFactDeclarations(raw2) {
3371
- const out = [];
3372
- for (const row of raw2 ?? []) {
3373
- if (!row || typeof row.name !== "string") continue;
3374
- const name = row.name.trim();
3375
- if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) continue;
3376
- const type = (row.type ?? "").trim();
3377
- if (type === "bool" || type === "number") {
3378
- out.push({ name, type });
3379
- } else if (type === "attestation") {
3380
- const binds = (row.binds ?? "none").toString().trim() || "none";
3381
- if (binds === "plan" || binds === "commit" || binds === "none") {
3382
- out.push({ name, type: "attestation", binds });
3383
- }
3384
- }
3385
- }
3386
- return out;
3387
- }
3388
3514
  function parseAgentCommand(value, agentId) {
3389
3515
  if (typeof value !== "string" || value.trim() === "") {
3390
3516
  throw new AgentConfigError(
@@ -3778,43 +3904,6 @@ function serializeStatusConfig(statuses) {
3778
3904
  }
3779
3905
  return lines.join("\n");
3780
3906
  }
3781
- function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
3782
- const problems = [];
3783
- const ids = new Set(statusConfig.statuses.map((s) => s.id));
3784
- if (derive.phaseLadder.length === 0) {
3785
- problems.push("phaseLadder must have at least one rung");
3786
- }
3787
- for (const rung of derive.phaseLadder) {
3788
- if (!ids.has(rung.phase)) {
3789
- problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
3790
- }
3791
- const err2 = rung.when === "*" ? null : validateWhen(rung.when);
3792
- if (err2) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err2}`);
3793
- }
3794
- const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
3795
- let sawElse = false;
3796
- for (const rule of derive.disposition) {
3797
- if (!VALID_DISPOSITIONS.has(rule.is)) {
3798
- problems.push(
3799
- `disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
3800
- );
3801
- }
3802
- if (rule.when === null) sawElse = true;
3803
- else {
3804
- const err2 = validateWhen(rule.when);
3805
- if (err2) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err2}`);
3806
- }
3807
- }
3808
- if (!sawElse) problems.push("disposition rules must end with an `else:` arm");
3809
- for (const key of ["parked", "blocked"]) {
3810
- if (!ids.has(derive.headline[key])) {
3811
- problems.push(
3812
- `headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
3813
- );
3814
- }
3815
- }
3816
- return problems;
3817
- }
3818
3907
  function serializeIntegrationConfig(integrations) {
3819
3908
  const lines = [];
3820
3909
  if (integrations.claudePluginDir) {
@@ -4881,7 +4970,7 @@ async function updateAgentsConfig(mutation, options = {}) {
4881
4970
  await writeAgentsConfig(next);
4882
4971
  return { previous, next, written: true };
4883
4972
  }
4884
- var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
4973
+ var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
4885
4974
  var init_config2 = __esm({
4886
4975
  "src/utils/config.ts"() {
4887
4976
  "use strict";
@@ -4893,38 +4982,10 @@ var init_config2 = __esm({
4893
4982
  init_hotkeysCatalog();
4894
4983
  init_agents_schema();
4895
4984
  init_slug();
4985
+ init_derive_config();
4896
4986
  init_fact_registry();
4897
4987
  init_terminal_schema();
4898
4988
  init_workspace_visibility_schema();
4899
- DEFAULT_DERIVE_CONFIG = {
4900
- phaseLadder: [
4901
- { phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
4902
- {
4903
- // planExists-but-not-approved also sits here: the default status set has
4904
- // no `planning` id. Users who define one add a `planExists:true` rung.
4905
- phase: "ready_for_planning",
4906
- when: "hasRealObjective:true AND acRealTotal > 0",
4907
- next: "Write a plan and get it approved"
4908
- },
4909
- { phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
4910
- {
4911
- phase: "in_progress",
4912
- when: "planApproved:true AND implementationStarted:true",
4913
- next: "Finish acceptance criteria, then request review"
4914
- },
4915
- {
4916
- phase: "review",
4917
- when: "acAllChecked:true OR reviewRequested:true",
4918
- next: "Complete, or address review feedback"
4919
- }
4920
- ],
4921
- disposition: [
4922
- { when: "parked:true", is: "parked" },
4923
- { when: "blocked:true", is: "blocked" },
4924
- { when: null, is: "active" }
4925
- ],
4926
- headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
4927
- };
4928
4989
  DEFAULT_ASSIGNMENT_TYPES = {
4929
4990
  definitions: [
4930
4991
  { id: "feature", label: "Feature" },
@@ -8784,6 +8845,8 @@ async function getStatusConfig() {
8784
8845
  order: effectiveOrder,
8785
8846
  transitions: effectiveTransitions2,
8786
8847
  transitionTable: buildTransitionTable(effectiveTransitions2),
8848
+ rawTransitions: sc.transitions,
8849
+ transitionsCustom: hasCustomTransitions,
8787
8850
  terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
8788
8851
  derive: sc.derive ?? null,
8789
8852
  facts: sc.facts ?? null,
@@ -8799,6 +8862,9 @@ async function getStatusConfig() {
8799
8862
  order: def.order,
8800
8863
  transitions: def.transitions,
8801
8864
  transitionTable: DEFAULT_TRANSITION_TABLE,
8865
+ // No custom config at all → the Settings editor shows read-only defaults.
8866
+ rawTransitions: [],
8867
+ transitionsCustom: false,
8802
8868
  terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"]),
8803
8869
  derive: null,
8804
8870
  facts: null,
@@ -9950,7 +10016,7 @@ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
9950
10016
  try {
9951
10017
  const { computeFactsDetailed: computeFactsDetailed2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
9952
10018
  const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
9953
- const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
10019
+ const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
9954
10020
  const { facts, attestations } = await computeFactsDetailed2({
9955
10021
  assignmentDir,
9956
10022
  frontmatter: {
@@ -9965,7 +10031,7 @@ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
9965
10031
  });
9966
10032
  const dims = deriveDimensions2({
9967
10033
  facts,
9968
- derive: config.derive ?? DEFAULT_DERIVE_CONFIG3,
10034
+ derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
9969
10035
  currentStatus: assignment.status,
9970
10036
  terminalStatuses: config.terminalStatuses,
9971
10037
  knownStatusIds: new Set(config.statuses.map((s) => s.id)),
@@ -19384,6 +19450,7 @@ function createWorkspaceVisibilityConfigRouter() {
19384
19450
  // src/dashboard/api-status-config.ts
19385
19451
  init_config2();
19386
19452
  init_fact_registry();
19453
+ init_state_machine();
19387
19454
  init_api();
19388
19455
  init_derive();
19389
19456
  import { Router as Router9 } from "express";
@@ -19715,6 +19782,25 @@ function buildAffectedResponse(id, list) {
19715
19782
  function isString(x) {
19716
19783
  return typeof x === "string" && x.length > 0;
19717
19784
  }
19785
+ function configResponse(config) {
19786
+ return {
19787
+ statuses: config.statuses,
19788
+ order: config.order,
19789
+ // RAW transitions (empty when the user has none) + a custom flag, so the
19790
+ // Settings editor can show read-only defaults vs. a customized table. The
19791
+ // materialized `config.transitions` (default-filled for runtime guards) is
19792
+ // deliberately NOT exposed here — sending it would make the defaults view
19793
+ // unreachable and round-trip phantom rows for undefined statuses.
19794
+ transitions: config.rawTransitions,
19795
+ transitionsCustom: config.transitionsCustom,
19796
+ custom: config.custom,
19797
+ derive: config.derive ?? DEFAULT_DERIVE_CONFIG,
19798
+ deriveCustom: config.derive !== null,
19799
+ knownCommands: [...DEFAULT_COMMAND_TARGETS.keys()],
19800
+ factDeclarations: config.factDeclarations,
19801
+ rawFacts: config.facts ?? []
19802
+ };
19803
+ }
19718
19804
  function parseResolutions(raw2) {
19719
19805
  if (raw2 === void 0) {
19720
19806
  return { resolutions: [], malformed: null, duplicateIds: null };
@@ -19788,14 +19874,7 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
19788
19874
  router.get("/", async (_req, res) => {
19789
19875
  try {
19790
19876
  const config = await getStatusConfig();
19791
- res.json({
19792
- statuses: config.statuses,
19793
- order: config.order,
19794
- transitions: config.transitions,
19795
- custom: config.custom,
19796
- factDeclarations: config.factDeclarations,
19797
- rawFacts: config.facts ?? []
19798
- });
19877
+ res.json(configResponse(config));
19799
19878
  } catch (error) {
19800
19879
  console.error("Error getting status config:", error);
19801
19880
  res.status(500).json({ error: "Failed to get status config" });
@@ -19822,6 +19901,7 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
19822
19901
  statuses,
19823
19902
  order,
19824
19903
  transitions,
19904
+ derive,
19825
19905
  facts: bodyFacts,
19826
19906
  factRemovalAcks,
19827
19907
  resolutions: rawResolutions
@@ -19831,17 +19911,26 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
19831
19911
  const hasStatuses = statuses !== void 0;
19832
19912
  const hasOrder = order !== void 0;
19833
19913
  const hasTransitions = transitions !== void 0;
19834
- let effectiveStatuses = statuses;
19835
- let effectiveOrder = order;
19836
- let effectiveTransitions2 = transitions;
19837
- if (hasFacts && !hasStatuses && !hasOrder && !hasTransitions) {
19838
- effectiveStatuses = currentConfig.statuses;
19839
- effectiveOrder = currentConfig.order;
19840
- effectiveTransitions2 = currentConfig.transitions;
19841
- } else if (!Array.isArray(effectiveStatuses) || !Array.isArray(effectiveOrder) || !Array.isArray(effectiveTransitions2)) {
19842
- res.status(400).json({ error: "malformed-statuses", message: "Request body must include statuses, order, and transitions arrays" });
19914
+ const hasDerive = derive !== void 0;
19915
+ if (!hasStatuses && !hasOrder && !hasTransitions && !hasFacts && !hasDerive) {
19916
+ res.status(400).json({ error: "malformed-statuses", message: "Request body must include at least one of statuses, order, transitions, derive, or facts" });
19917
+ return;
19918
+ }
19919
+ if (hasStatuses && !Array.isArray(statuses)) {
19920
+ res.status(400).json({ error: "malformed-statuses", message: "statuses must be an array" });
19921
+ return;
19922
+ }
19923
+ if (hasOrder && !Array.isArray(order)) {
19924
+ res.status(400).json({ error: "malformed-statuses", message: "order must be an array" });
19843
19925
  return;
19844
19926
  }
19927
+ if (hasTransitions && !Array.isArray(transitions)) {
19928
+ res.status(400).json({ error: "malformed-transitions", message: "transitions must be an array" });
19929
+ return;
19930
+ }
19931
+ const effectiveStatuses = hasStatuses ? statuses : currentConfig.statuses;
19932
+ const effectiveOrder = hasOrder ? order : currentConfig.order;
19933
+ const effectiveTransitions2 = hasTransitions ? transitions : currentConfig.rawTransitions;
19845
19934
  const parsed = parseResolutions(rawResolutions);
19846
19935
  if (parsed.malformed) {
19847
19936
  res.status(400).json({ error: "malformed-resolutions", message: parsed.malformed });
@@ -19888,60 +19977,13 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
19888
19977
  return;
19889
19978
  }
19890
19979
  }
19891
- let affectedMap;
19892
- try {
19893
- affectedMap = await scanAssignmentsByStatus(projectsDir2, assignmentsDir2, droppedIds);
19894
- } catch (err2) {
19895
- if (err2 instanceof StatusResolutionError) {
19896
- const mapped = mapResolutionErrorToHttp(err2, null);
19897
- res.status(mapped.status).json(mapped.body);
19898
- return;
19899
- }
19900
- throw err2;
19901
- }
19902
- const unresolved = [];
19903
- const resolvedById = new Set(resolutions.map((r) => r.id));
19904
- for (const id of droppedIds) {
19905
- const list = affectedMap.get(id) ?? [];
19906
- if (list.length > 0 && !resolvedById.has(id)) {
19907
- unresolved.push(buildAffectedResponse(id, list));
19908
- }
19909
- }
19910
- if (unresolved.length > 0) {
19911
- res.status(409).json({ error: "unresolved-orphans", unresolved });
19912
- return;
19913
- }
19914
- let applied;
19915
- try {
19916
- applied = await applyStatusResolutions(resolutions, affectedMap, validTargets);
19917
- } catch (err2) {
19918
- if (err2 instanceof StatusResolutionError) {
19919
- const mapped = mapResolutionErrorToHttp(err2, null);
19920
- res.status(mapped.status).json(mapped.body);
19921
- return;
19922
- }
19923
- throw err2;
19924
- }
19925
- try {
19926
- await verifyNoDriftedOrphans(projectsDir2, assignmentsDir2, droppedIds);
19927
- } catch (err2) {
19928
- if (err2 instanceof StatusResolutionError) {
19929
- const mapped = mapResolutionErrorToHttp(err2, {
19930
- remapped: applied.remapped,
19931
- deleted: applied.deleted
19932
- });
19933
- res.status(mapped.status).json(mapped.body);
19934
- return;
19935
- }
19936
- throw err2;
19937
- }
19938
19980
  let factsToWrite = currentConfig.facts ?? null;
19939
19981
  if (hasFacts) {
19940
- const shapedFacts = [];
19941
19982
  if (!Array.isArray(bodyFacts)) {
19942
19983
  res.status(400).json({ error: "malformed-facts", message: "facts must be an array" });
19943
19984
  return;
19944
19985
  }
19986
+ const shapedFacts = [];
19945
19987
  for (let i = 0; i < bodyFacts.length; i++) {
19946
19988
  const row = bodyFacts[i];
19947
19989
  if (!row || typeof row !== "object") {
@@ -19963,24 +20005,73 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
19963
20005
  res.status(400).json({ error: "invalid-facts", problems });
19964
20006
  return;
19965
20007
  }
20008
+ factsToWrite = shapedFacts;
20009
+ }
20010
+ let effectiveDerive = currentConfig.derive ?? null;
20011
+ if (hasDerive) {
20012
+ if (derive === null) {
20013
+ effectiveDerive = null;
20014
+ } else {
20015
+ const shapeProblems = validateDeriveShape(derive);
20016
+ if (shapeProblems.length > 0) {
20017
+ res.status(400).json({ error: "invalid-derive", problems: shapeProblems });
20018
+ return;
20019
+ }
20020
+ const incomingRegistry = buildDeriveRegistry(
20021
+ acceptFactDeclarations(normalizeFactDeclarations(factsToWrite))
20022
+ );
20023
+ const problems = validateDeriveConfig(
20024
+ derive,
20025
+ { statuses: effectiveStatuses },
20026
+ (when) => validateDeriveCondition(when, incomingRegistry)
20027
+ );
20028
+ if (problems.length > 0) {
20029
+ res.status(400).json({ error: "invalid-derive", problems });
20030
+ return;
20031
+ }
20032
+ effectiveDerive = derive;
20033
+ }
20034
+ }
20035
+ if (hasTransitions) {
20036
+ for (let i = 0; i < transitions.length; i++) {
20037
+ const t = transitions[i];
20038
+ const ok = !!t && typeof t === "object" && typeof t.from === "string" && typeof t.command === "string" && typeof t.to === "string" && (t.label === void 0 || typeof t.label === "string") && (t.description === void 0 || typeof t.description === "string") && (t.requiresReason === void 0 || typeof t.requiresReason === "boolean");
20039
+ if (!ok) {
20040
+ res.status(400).json({
20041
+ error: "invalid-transitions",
20042
+ problems: [`transitions[${i}] must have string from/command/to (+ optional string label/description, boolean requiresReason)`]
20043
+ });
20044
+ return;
20045
+ }
20046
+ }
20047
+ const tproblems = [];
20048
+ for (const t of transitions) {
20049
+ if (!newIds.has(t.from)) tproblems.push(`transition ${t.from} --${t.command}--> ${t.to}: "${t.from}" is not a defined status`);
20050
+ if (!newIds.has(t.to)) tproblems.push(`transition ${t.from} --${t.command}--> ${t.to}: "${t.to}" is not a defined status`);
20051
+ }
20052
+ if (tproblems.length > 0) {
20053
+ res.status(400).json({ error: "invalid-transitions", problems: tproblems });
20054
+ return;
20055
+ }
20056
+ }
20057
+ if (hasFacts) {
19966
20058
  const currentNames = new Set((currentConfig.facts ?? []).map((f) => f.name));
19967
- const incomingNames = new Set(shapedFacts.map((f) => f.name));
20059
+ const incomingNames = new Set((factsToWrite ?? []).map((f) => f.name));
19968
20060
  const removedNames = [];
19969
20061
  for (const name of currentNames) {
19970
20062
  if (!incomingNames.has(name)) removedNames.push(name);
19971
20063
  }
19972
20064
  const acks = new Set(Array.isArray(factRemovalAcks) ? factRemovalAcks.map((x) => String(x)) : []);
19973
20065
  const unresolvedRefs = [];
19974
- const deriveConfig = currentConfig.derive ?? null;
19975
- if (deriveConfig !== null && removedNames.length > 0) {
20066
+ if (effectiveDerive !== null && removedNames.length > 0) {
19976
20067
  const acceptedAll = acceptFactDeclarations(normalizeFactDeclarations(currentConfig.facts));
19977
20068
  const fullRegistry = buildDeriveRegistry(acceptedAll);
19978
20069
  for (const removedName of removedNames) {
19979
20070
  if (acks.has(removedName)) continue;
19980
20071
  const acceptedWithout = acceptedAll.filter((d) => d.name !== removedName);
19981
20072
  const withoutRegistry = buildDeriveRegistry(acceptedWithout);
19982
- for (let i = 0; i < deriveConfig.phaseLadder.length; i++) {
19983
- const rung = deriveConfig.phaseLadder[i];
20073
+ for (let i = 0; i < effectiveDerive.phaseLadder.length; i++) {
20074
+ const rung = effectiveDerive.phaseLadder[i];
19984
20075
  if (rung.when === "*") continue;
19985
20076
  const before = validateDeriveCondition(rung.when, fullRegistry);
19986
20077
  const after = validateDeriveCondition(rung.when, withoutRegistry);
@@ -19988,8 +20079,8 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
19988
20079
  unresolvedRefs.push({ factName: removedName, location: `phaseLadder[${i}]`, when: rung.when });
19989
20080
  }
19990
20081
  }
19991
- for (let i = 0; i < deriveConfig.disposition.length; i++) {
19992
- const rule = deriveConfig.disposition[i];
20082
+ for (let i = 0; i < effectiveDerive.disposition.length; i++) {
20083
+ const rule = effectiveDerive.disposition[i];
19993
20084
  if (rule.when === null) continue;
19994
20085
  const before = validateDeriveCondition(rule.when, fullRegistry);
19995
20086
  const after = validateDeriveCondition(rule.when, withoutRegistry);
@@ -20003,14 +20094,60 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
20003
20094
  res.status(409).json({ error: "unresolved-fact-references", references: unresolvedRefs });
20004
20095
  return;
20005
20096
  }
20006
- factsToWrite = shapedFacts;
20097
+ }
20098
+ let affectedMap;
20099
+ try {
20100
+ affectedMap = await scanAssignmentsByStatus(projectsDir2, assignmentsDir2, droppedIds);
20101
+ } catch (err2) {
20102
+ if (err2 instanceof StatusResolutionError) {
20103
+ const mapped = mapResolutionErrorToHttp(err2, null);
20104
+ res.status(mapped.status).json(mapped.body);
20105
+ return;
20106
+ }
20107
+ throw err2;
20108
+ }
20109
+ const unresolved = [];
20110
+ const resolvedById = new Set(resolutions.map((r) => r.id));
20111
+ for (const id of droppedIds) {
20112
+ const list = affectedMap.get(id) ?? [];
20113
+ if (list.length > 0 && !resolvedById.has(id)) {
20114
+ unresolved.push(buildAffectedResponse(id, list));
20115
+ }
20116
+ }
20117
+ if (unresolved.length > 0) {
20118
+ res.status(409).json({ error: "unresolved-orphans", unresolved });
20119
+ return;
20120
+ }
20121
+ let applied;
20122
+ try {
20123
+ applied = await applyStatusResolutions(resolutions, affectedMap, validTargets);
20124
+ } catch (err2) {
20125
+ if (err2 instanceof StatusResolutionError) {
20126
+ const mapped = mapResolutionErrorToHttp(err2, null);
20127
+ res.status(mapped.status).json(mapped.body);
20128
+ return;
20129
+ }
20130
+ throw err2;
20131
+ }
20132
+ try {
20133
+ await verifyNoDriftedOrphans(projectsDir2, assignmentsDir2, droppedIds);
20134
+ } catch (err2) {
20135
+ if (err2 instanceof StatusResolutionError) {
20136
+ const mapped = mapResolutionErrorToHttp(err2, {
20137
+ remapped: applied.remapped,
20138
+ deleted: applied.deleted
20139
+ });
20140
+ res.status(mapped.status).json(mapped.body);
20141
+ return;
20142
+ }
20143
+ throw err2;
20007
20144
  }
20008
20145
  try {
20009
20146
  await writeStatusConfig({
20010
20147
  statuses: effectiveStatuses,
20011
20148
  order: effectiveOrder,
20012
20149
  transitions: effectiveTransitions2,
20013
- derive: currentConfig.derive ?? null,
20150
+ derive: effectiveDerive,
20014
20151
  facts: factsToWrite
20015
20152
  });
20016
20153
  } catch (err2) {
@@ -20029,10 +20166,7 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
20029
20166
  byId[id] = entry.target !== void 0 ? { mode: entry.mode, count: entry.count, target: entry.target } : { mode: entry.mode, count: entry.count };
20030
20167
  }
20031
20168
  res.json({
20032
- statuses: config.statuses,
20033
- order: config.order,
20034
- transitions: config.transitions,
20035
- custom: config.custom,
20169
+ ...configResponse(config),
20036
20170
  applied: { remapped: applied.remapped, deleted: applied.deleted, byId }
20037
20171
  });
20038
20172
  } catch (error) {
@@ -20045,12 +20179,7 @@ function createStatusConfigRouter(projectsDir2, assignmentsDir2) {
20045
20179
  await deleteStatusConfig();
20046
20180
  clearStatusConfigCache();
20047
20181
  const config = await getStatusConfig();
20048
- res.json({
20049
- statuses: config.statuses,
20050
- order: config.order,
20051
- transitions: config.transitions,
20052
- custom: config.custom
20053
- });
20182
+ res.json(configResponse(config));
20054
20183
  } catch (error) {
20055
20184
  console.error("Error resetting status config:", error);
20056
20185
  res.status(500).json({ error: "Failed to reset status config" });