hanoman 0.1.20 → 0.1.22

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 (49) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli.js +52 -6
  3. package/dist/server.js +308 -102
  4. package/package.json +1 -1
  5. package/prisma/migrations/20260806000000_spec_source_history/migration.sql +10 -0
  6. package/prisma/schema.prisma +7 -0
  7. package/web/assets/hnm-ill-diagram-technical-kit-modular-master-v01-DMmzOITA.webp +0 -0
  8. package/web/assets/hnm-ill-hero-workflow-16x9-master-v01-BngPlVl5.webp +0 -0
  9. package/web/assets/hnm-ill-lakon-anoman-duta-4x3-master-v01-0hEUEZ6d.webp +0 -0
  10. package/web/assets/hnm-ill-lakon-anoman-obong-4x3-master-v01-D841uTR5.webp +0 -0
  11. package/web/assets/hnm-ill-lakon-chiranjivi-4x3-master-v01-DtbnhQ7C.webp +0 -0
  12. package/web/assets/hnm-ill-lakon-gunung-dronagiri-4x3-master-v01-C6nCvUhL.webp +0 -0
  13. package/web/assets/hnm-ill-mascot-pose-ask-1x1-master-v01-S3NAOxD0.webp +0 -0
  14. package/web/assets/hnm-ill-mascot-pose-carry-knowledge-1x1-master-v01-BnkGXS8s.webp +0 -0
  15. package/web/assets/hnm-ill-mascot-pose-celebrate-1x1-master-v01-B-OkKay_.webp +0 -0
  16. package/web/assets/hnm-ill-mascot-pose-neutral-1x1-master-v01-CnvdPWgA.webp +0 -0
  17. package/web/assets/hnm-ill-mascot-pose-observe-1x1-master-v01-CG_jUBwM.webp +0 -0
  18. package/web/assets/hnm-ill-mascot-pose-warn-1x1-master-v01-Cm_KnXCE.webp +0 -0
  19. package/web/assets/hnm-ill-mascot-pose-welcome-1x1-master-v01-CAaHl0C1.webp +0 -0
  20. package/web/assets/hnm-ill-mascot-pose-work-1x1-master-v01-CFT1_F13.webp +0 -0
  21. package/web/assets/hnm-ill-model-character-sheet-master-v01-CIs0yaMj.webp +0 -0
  22. package/web/assets/hnm-ill-model-mascot-sheet-master-v01-BNJl7gCC.webp +0 -0
  23. package/web/assets/hnm-ill-motif-lakon-buntut-tile-strip-master-v01-DUZOJrQs.webp +0 -0
  24. package/web/assets/hnm-ill-product-state-awaiting-decision-4x3-master-v01-BwYfsI4Y.webp +0 -0
  25. package/web/assets/hnm-ill-product-state-empty-backlog-4x3-master-v01-CpttzxOY.webp +0 -0
  26. package/web/assets/hnm-ill-product-state-onboarding-4x3-master-v01-BEqlvDgy.webp +0 -0
  27. package/web/assets/hnm-ill-product-state-recoverable-error-4x3-master-v01-BcVo2BbN.webp +0 -0
  28. package/web/assets/hnm-ill-product-state-session-active-4x3-master-v01-BErsEZ5E.webp +0 -0
  29. package/web/assets/hnm-ill-product-state-success-4x3-master-v01-tJsjz-0L.webp +0 -0
  30. package/web/assets/hnm-ill-social-landscape-16x9-master-v01-DCmpP1dP.webp +0 -0
  31. package/web/assets/hnm-ill-social-portrait-4x5-master-v01-DAMYmb8y.webp +0 -0
  32. package/web/assets/hnm-ill-social-square-1x1-master-v01-CA5ntGVr.webp +0 -0
  33. package/web/assets/hnm-ill-social-story-9x16-master-v01-DCZ-dWfx.webp +0 -0
  34. package/web/assets/hnm-ill-spot-context-1x1-master-v01-C-_dXTKT.webp +0 -0
  35. package/web/assets/hnm-ill-spot-durable-knowledge-1x1-master-v01-_CVUcBE4.webp +0 -0
  36. package/web/assets/hnm-ill-spot-human-control-1x1-master-v01-BRV52OwQ.webp +0 -0
  37. package/web/assets/hnm-ill-spot-isolation-1x1-master-v01-C6wtR8uB.webp +0 -0
  38. package/web/assets/hnm-ill-spot-parallel-work-1x1-master-v01-Ch4Wl1Du.webp +0 -0
  39. package/web/assets/hnm-ill-spot-visibility-1x1-master-v01-DkgTZ2MF.webp +0 -0
  40. package/web/assets/hnm-ill-sticker-blocked-1x1-master-v01-suto6sZ9.webp +0 -0
  41. package/web/assets/hnm-ill-sticker-docs-updated-1x1-master-v01-CV0BaOI1.webp +0 -0
  42. package/web/assets/hnm-ill-sticker-ready-1x1-master-v01-Cetao79T.webp +0 -0
  43. package/web/assets/hnm-ill-sticker-review-1x1-master-v01-txPj5Zvx.webp +0 -0
  44. package/web/assets/hnm-ill-sticker-shipped-1x1-master-v01-r05qU9m-.webp +0 -0
  45. package/web/assets/hnm-ill-sticker-thanks-1x1-master-v01-BlHuF8Nt.webp +0 -0
  46. package/web/assets/hnm-ill-sticker-waiting-1x1-master-v01-Dh-UnLNA.webp +0 -0
  47. package/web/assets/hnm-ill-sticker-working-1x1-master-v01-TQU0v2gf.webp +0 -0
  48. package/web/assets/{index-BJTj--gG.js → index-Di1yjym7.js} +1547 -1547
  49. package/web/index.html +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.20",
3
- "sha": "76f8304",
4
- "builtAt": "2026-08-06T02:10:06.880Z"
2
+ "version": "0.1.22",
3
+ "sha": "c3a0a34",
4
+ "builtAt": "2026-08-08T07:24:55.380Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -4301,6 +4301,24 @@ var init_enums = __esm({
4301
4301
  }
4302
4302
  });
4303
4303
 
4304
+ // ../shared/src/spec-source.ts
4305
+ function payloadShapeFor(source) {
4306
+ return source === "qa" ? "qa" : source === "goal" ? "goal" : "brief";
4307
+ }
4308
+ function shapeOfPayload(payload) {
4309
+ const p = payload ?? {};
4310
+ return "severity" in p ? "qa" : "goal" in p ? "goal" : "brief";
4311
+ }
4312
+ function payloadMatchesSource(source, payload) {
4313
+ return shapeOfPayload(payload) === payloadShapeFor(source);
4314
+ }
4315
+ var init_spec_source = __esm({
4316
+ "../shared/src/spec-source.ts"() {
4317
+ "use strict";
4318
+ init_enums();
4319
+ }
4320
+ });
4321
+
4304
4322
  // ../shared/src/agent-engine.ts
4305
4323
  var zAgentEngine;
4306
4324
  var init_agent_engine = __esm({
@@ -4438,7 +4456,7 @@ var init_auto_merge = __esm({
4438
4456
  });
4439
4457
 
4440
4458
  // ../shared/src/entities.ts
4441
- var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4459
+ var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zSpec, NOTIFY_SOUNDS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4442
4460
  var init_entities = __esm({
4443
4461
  "../shared/src/entities.ts"() {
4444
4462
  "use strict";
@@ -4488,6 +4506,13 @@ var init_entities = __esm({
4488
4506
  id: external_exports.string(),
4489
4507
  reason: external_exports.enum(["missing", "unfinished", "unmerged"])
4490
4508
  });
4509
+ zSourceChange = external_exports.object({
4510
+ at: external_exports.string(),
4511
+ from: external_exports.string(),
4512
+ to: external_exports.string(),
4513
+ by: external_exports.string(),
4514
+ payload: external_exports.unknown().optional()
4515
+ });
4491
4516
  zSpec = external_exports.object({
4492
4517
  id: external_exports.string(),
4493
4518
  projectId: external_exports.string(),
@@ -4515,7 +4540,11 @@ var init_entities = __esm({
4515
4540
  blockedBy: external_exports.array(zSpecBlocker).default([]),
4516
4541
  // SPEC-486 · ADR-0103 · override kebijakan auto-merge item ini; null = warisi project.
4517
4542
  // `.nullable().default(null)` menjaga respons/klien versi lama tetap parse.
4518
- autoMerge: zAutoMerge.nullable().default(null)
4543
+ autoMerge: zAutoMerge.nullable().default(null),
4544
+ // SPEC-546 · ADR-0109 · jejak konversi type. `.default([])` menjaga respons/klien versi lama
4545
+ // tetap parse; kolom DB-nya `Json?` sehingga baris yang belum pernah dikonversi mengirim
4546
+ // `null` — pemakai UI menulis `spec.sourceHistory ?? []`, cermin `blockedBy`.
4547
+ sourceHistory: external_exports.array(zSourceChange).default([])
4519
4548
  });
4520
4549
  NOTIFY_SOUNDS = [
4521
4550
  "off",
@@ -4957,7 +4986,7 @@ var init_prd_status = __esm({
4957
4986
  });
4958
4987
 
4959
4988
  // ../shared/src/dto.ts
4960
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
4989
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
4961
4990
  var init_dto = __esm({
4962
4991
  "../shared/src/dto.ts"() {
4963
4992
  "use strict";
@@ -4967,6 +4996,7 @@ var init_dto = __esm({
4967
4996
  init_auto_merge();
4968
4997
  init_prd_status();
4969
4998
  init_enums();
4999
+ init_spec_source();
4970
5000
  zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
4971
5001
  zSessionResult = external_exports.object({
4972
5002
  id: external_exports.string(),
@@ -5034,9 +5064,7 @@ var init_dto = __esm({
5034
5064
  // SPEC-447 · ADR-0093 · divalidasi server (id ada / satu project / bukan diri sendiri / non-siklus).
5035
5065
  dependsOn: external_exports.array(external_exports.string()).optional()
5036
5066
  }).superRefine((o, ctx) => {
5037
- const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
5038
- const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
5039
- if (shape !== want)
5067
+ if (!payloadMatchesSource(o.source, o.payload))
5040
5068
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
5041
5069
  });
5042
5070
  zPatchSpec = external_exports.object({
@@ -5057,6 +5085,13 @@ var init_dto = __esm({
5057
5085
  // apa yang terjadi SESUDAH kerja, bukan konten yang sedang dikerjakan sesi hidup.
5058
5086
  autoMerge: zAutoMerge.nullable().optional()
5059
5087
  });
5088
+ zChangeSpecSource = external_exports.object({
5089
+ source: zSpecSource,
5090
+ payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
5091
+ }).superRefine((o, ctx) => {
5092
+ if (o.payload !== void 0 && !payloadMatchesSource(o.source, o.payload))
5093
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
5094
+ });
5060
5095
  zIntegrate = external_exports.object({
5061
5096
  op: external_exports.enum(["merge", "rebase"]),
5062
5097
  target: external_exports.string().regex(/^(local|origin):.+/)
@@ -5454,6 +5489,8 @@ var init_api = __esm({
5454
5489
  // SPEC-340 · ADR-0076 · rekomendasi tindak lanjut audit (turunan blok json dokumen audit).
5455
5490
  specEscalation: (id) => `${API}/specs/${id}/escalation`,
5456
5491
  specIntegrate: (id) => `${API}/specs/${id}/integrate`,
5492
+ // SPEC-546 · ADR-0109 · ubah type/source item in-place (operasi khusus, bukan field PATCH).
5493
+ specSource: (id) => `${API}/specs/${id}/source`,
5457
5494
  specReview: (id) => `${API}/specs/${id}/review`,
5458
5495
  specReviewFile: (id, path) => `${API}/specs/${id}/review/${path}`,
5459
5496
  settings: `${API}/settings`,
@@ -6009,6 +6046,14 @@ var init_webhook = __esm({
6009
6046
  label: "Stage backlog berpindah",
6010
6047
  changed: ["stage"],
6011
6048
  when: "Stage berpindah \u2014 baik oleh fase sesi yang tercatat (otomatis) maupun revert manual operator. Menggantikan spec.updated untuk perubahan itu."
6049
+ }, {
6050
+ // SPEC-546 · ADR-0109 · konversi type item. Pola yang sama dengan stage_changed: peristiwa
6051
+ // turunan MENGGANTIKAN spec.updated, supaya penerima bisa bereaksi pada "type berpindah"
6052
+ // tanpa mendiff dua amplop.
6053
+ type: "spec.source_changed",
6054
+ label: "Type backlog berpindah",
6055
+ changed: ["source"],
6056
+ when: "Type/source item backlog dikonversi lewat POST /specs/:id/source (mis. brief \u2192 qa). Menggantikan spec.updated untuk perubahan itu."
6012
6057
  }],
6013
6058
  sample: {
6014
6059
  id: "SPEC-481",
@@ -6965,6 +7010,7 @@ var init_src = __esm({
6965
7010
  "../shared/src/index.ts"() {
6966
7011
  "use strict";
6967
7012
  init_enums();
7013
+ init_spec_source();
6968
7014
  init_agent_engine();
6969
7015
  init_entities();
6970
7016
  init_agent();
package/dist/server.js CHANGED
@@ -4164,6 +4164,98 @@ var init_enums = __esm({
4164
4164
  }
4165
4165
  });
4166
4166
 
4167
+ // ../shared/src/spec-source.ts
4168
+ function payloadShapeFor(source) {
4169
+ return source === "qa" ? "qa" : source === "goal" ? "goal" : "brief";
4170
+ }
4171
+ function shapeOfPayload(payload) {
4172
+ const p = payload ?? {};
4173
+ return "severity" in p ? "qa" : "goal" in p ? "goal" : "brief";
4174
+ }
4175
+ function payloadMatchesSource(source, payload) {
4176
+ return shapeOfPayload(payload) === payloadShapeFor(source);
4177
+ }
4178
+ function priorityFromSeverity(severity) {
4179
+ return severity === "minor" ? "sedang" : "tinggi";
4180
+ }
4181
+ function severityFromPriority(priority) {
4182
+ return priority === "tinggi" ? "major" : "minor";
4183
+ }
4184
+ function convertPayload(to, payload) {
4185
+ const p = payload ?? {};
4186
+ const str3 = (k) => typeof p[k] === "string" ? p[k] : "";
4187
+ const prio = () => p.priority === "tinggi" || p.priority === "rendah" ? p.priority : "sedang";
4188
+ const nonEmpty = (fields) => fields.filter((f) => str3(f) !== "");
4189
+ const fromShape = shapeOfPayload(payload);
4190
+ const toShape = payloadShapeFor(to);
4191
+ const fromAudit = str3("fromAudit");
4192
+ const done = (out3, dropped) => ({
4193
+ payload: out3,
4194
+ dropped,
4195
+ missing: SHAPE_REQUIRED[toShape].filter((f) => typeof out3[f] !== "string" || out3[f] === "")
4196
+ });
4197
+ if (fromShape === toShape) return { payload: { ...p }, dropped: [], missing: [] };
4198
+ if (toShape === "qa") {
4199
+ if (fromShape === "brief")
4200
+ return done({
4201
+ severity: severityFromPriority(prio()),
4202
+ steps: "",
4203
+ expected: str3("outcome"),
4204
+ actual: str3("context"),
4205
+ env: "",
4206
+ ...fromAudit ? { fromAudit } : {}
4207
+ }, nonEmpty(["constraints"]));
4208
+ return done({
4209
+ severity: severityFromPriority(prio()),
4210
+ steps: "",
4211
+ expected: str3("goal"),
4212
+ actual: "",
4213
+ env: ""
4214
+ }, nonEmpty(["done", "constraints"]));
4215
+ }
4216
+ if (toShape === "goal") {
4217
+ if (fromShape === "brief") {
4218
+ const goal = str3("outcome") || str3("context");
4219
+ return done(
4220
+ { goal, done: "", constraints: str3("constraints"), priority: prio() },
4221
+ nonEmpty([...str3("outcome") ? ["context"] : [], "fromAudit"])
4222
+ );
4223
+ }
4224
+ return done({
4225
+ goal: str3("expected"),
4226
+ done: "",
4227
+ constraints: "",
4228
+ priority: priorityFromSeverity(p.severity)
4229
+ }, nonEmpty(["steps", "actual", "env", "fromAudit"]));
4230
+ }
4231
+ if (fromShape === "qa")
4232
+ return done({
4233
+ context: str3("actual"),
4234
+ outcome: str3("expected"),
4235
+ constraints: "",
4236
+ priority: priorityFromSeverity(p.severity),
4237
+ ...fromAudit ? { fromAudit } : {}
4238
+ }, nonEmpty(["steps", "env"]));
4239
+ return done({
4240
+ context: "",
4241
+ outcome: str3("goal"),
4242
+ constraints: str3("constraints"),
4243
+ priority: prio()
4244
+ }, nonEmpty(["done"]));
4245
+ }
4246
+ var SHAPE_REQUIRED;
4247
+ var init_spec_source = __esm({
4248
+ "../shared/src/spec-source.ts"() {
4249
+ "use strict";
4250
+ init_enums();
4251
+ SHAPE_REQUIRED = {
4252
+ brief: ["context", "outcome"],
4253
+ qa: ["steps", "expected", "actual", "env"],
4254
+ goal: ["goal", "done"]
4255
+ };
4256
+ }
4257
+ });
4258
+
4167
4259
  // ../shared/src/agent-engine.ts
4168
4260
  var zAgentEngine;
4169
4261
  var init_agent_engine = __esm({
@@ -4341,7 +4433,7 @@ function cmpVersion(a, b) {
4341
4433
  }
4342
4434
  return 0;
4343
4435
  }
4344
- var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4436
+ var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zSpec, NOTIFY_SOUNDS, MODELS, EFFORTS, E_5_6, E_LUNA, E_BASE, CODEX_MODELS, RETIRED_CODEX_MODELS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, CHANGELOG_ENGINE_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
4345
4437
  var init_entities = __esm({
4346
4438
  "../shared/src/entities.ts"() {
4347
4439
  "use strict";
@@ -4391,6 +4483,13 @@ var init_entities = __esm({
4391
4483
  id: external_exports.string(),
4392
4484
  reason: external_exports.enum(["missing", "unfinished", "unmerged"])
4393
4485
  });
4486
+ zSourceChange = external_exports.object({
4487
+ at: external_exports.string(),
4488
+ from: external_exports.string(),
4489
+ to: external_exports.string(),
4490
+ by: external_exports.string(),
4491
+ payload: external_exports.unknown().optional()
4492
+ });
4394
4493
  zSpec = external_exports.object({
4395
4494
  id: external_exports.string(),
4396
4495
  projectId: external_exports.string(),
@@ -4418,7 +4517,11 @@ var init_entities = __esm({
4418
4517
  blockedBy: external_exports.array(zSpecBlocker).default([]),
4419
4518
  // SPEC-486 · ADR-0103 · override kebijakan auto-merge item ini; null = warisi project.
4420
4519
  // `.nullable().default(null)` menjaga respons/klien versi lama tetap parse.
4421
- autoMerge: zAutoMerge.nullable().default(null)
4520
+ autoMerge: zAutoMerge.nullable().default(null),
4521
+ // SPEC-546 · ADR-0109 · jejak konversi type. `.default([])` menjaga respons/klien versi lama
4522
+ // tetap parse; kolom DB-nya `Json?` sehingga baris yang belum pernah dikonversi mengirim
4523
+ // `null` — pemakai UI menulis `spec.sourceHistory ?? []`, cermin `blockedBy`.
4524
+ sourceHistory: external_exports.array(zSourceChange).default([])
4422
4525
  });
4423
4526
  NOTIFY_SOUNDS = [
4424
4527
  "off",
@@ -5134,7 +5237,7 @@ var init_prd_status = __esm({
5134
5237
  function flowForSource(source) {
5135
5238
  return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
5136
5239
  }
5137
- var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
5240
+ var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
5138
5241
  var init_dto = __esm({
5139
5242
  "../shared/src/dto.ts"() {
5140
5243
  "use strict";
@@ -5144,6 +5247,7 @@ var init_dto = __esm({
5144
5247
  init_auto_merge();
5145
5248
  init_prd_status();
5146
5249
  init_enums();
5250
+ init_spec_source();
5147
5251
  zIssueDeviceToken = external_exports.object({ name: external_exports.string().min(1) });
5148
5252
  zSessionResult = external_exports.object({
5149
5253
  id: external_exports.string(),
@@ -5211,9 +5315,7 @@ var init_dto = __esm({
5211
5315
  // SPEC-447 · ADR-0093 · divalidasi server (id ada / satu project / bukan diri sendiri / non-siklus).
5212
5316
  dependsOn: external_exports.array(external_exports.string()).optional()
5213
5317
  }).superRefine((o, ctx) => {
5214
- const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
5215
- const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
5216
- if (shape !== want)
5318
+ if (!payloadMatchesSource(o.source, o.payload))
5217
5319
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
5218
5320
  });
5219
5321
  zPatchSpec = external_exports.object({
@@ -5234,6 +5336,13 @@ var init_dto = __esm({
5234
5336
  // apa yang terjadi SESUDAH kerja, bukan konten yang sedang dikerjakan sesi hidup.
5235
5337
  autoMerge: zAutoMerge.nullable().optional()
5236
5338
  });
5339
+ zChangeSpecSource = external_exports.object({
5340
+ source: zSpecSource,
5341
+ payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
5342
+ }).superRefine((o, ctx) => {
5343
+ if (o.payload !== void 0 && !payloadMatchesSource(o.source, o.payload))
5344
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
5345
+ });
5237
5346
  zIntegrate = external_exports.object({
5238
5347
  op: external_exports.enum(["merge", "rebase"]),
5239
5348
  target: external_exports.string().regex(/^(local|origin):.+/)
@@ -5631,6 +5740,8 @@ var init_api = __esm({
5631
5740
  // SPEC-340 · ADR-0076 · rekomendasi tindak lanjut audit (turunan blok json dokumen audit).
5632
5741
  specEscalation: (id) => `${API}/specs/${id}/escalation`,
5633
5742
  specIntegrate: (id) => `${API}/specs/${id}/integrate`,
5743
+ // SPEC-546 · ADR-0109 · ubah type/source item in-place (operasi khusus, bukan field PATCH).
5744
+ specSource: (id) => `${API}/specs/${id}/source`,
5634
5745
  specReview: (id) => `${API}/specs/${id}/review`,
5635
5746
  specReviewFile: (id, path) => `${API}/specs/${id}/review/${path}`,
5636
5747
  settings: `${API}/settings`,
@@ -6311,6 +6422,14 @@ var init_webhook = __esm({
6311
6422
  label: "Stage backlog berpindah",
6312
6423
  changed: ["stage"],
6313
6424
  when: "Stage berpindah \u2014 baik oleh fase sesi yang tercatat (otomatis) maupun revert manual operator. Menggantikan spec.updated untuk perubahan itu."
6425
+ }, {
6426
+ // SPEC-546 · ADR-0109 · konversi type item. Pola yang sama dengan stage_changed: peristiwa
6427
+ // turunan MENGGANTIKAN spec.updated, supaya penerima bisa bereaksi pada "type berpindah"
6428
+ // tanpa mendiff dua amplop.
6429
+ type: "spec.source_changed",
6430
+ label: "Type backlog berpindah",
6431
+ changed: ["source"],
6432
+ when: "Type/source item backlog dikonversi lewat POST /specs/:id/source (mis. brief \u2192 qa). Menggantikan spec.updated untuk perubahan itu."
6314
6433
  }],
6315
6434
  sample: {
6316
6435
  id: "SPEC-481",
@@ -7250,6 +7369,7 @@ var init_src = __esm({
7250
7369
  "../shared/src/index.ts"() {
7251
7370
  "use strict";
7252
7371
  init_enums();
7372
+ init_spec_source();
7253
7373
  init_agent_engine();
7254
7374
  init_entities();
7255
7375
  init_agent();
@@ -9749,7 +9869,12 @@ var init_sync = __esm({
9749
9869
  // SPEC-516 · ADR-0105 · doneAt ikut menyeberang — cermin createdAt/startedAt. Tanpa ini spec
9750
9870
  // asal-hub mendarat di tiap client dengan doneAt null tanpa satu pun error, dan changelog
9751
9871
  // mode backlog di client itu selamanya kosong.
9752
- spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "createdAt", "startedAt", "doneAt", "updatedAt"],
9872
+ // SPEC-546 · ADR-0109 · sourceHistory ikut menyeberang: jejak konversi type adalah bagian
9873
+ // keadaan yang harus dilihat sama oleh semua mesin. `upsert` yang tak menyebut sebuah kolom
9874
+ // TETAP berhasil, jadi kolom yang terlewat di sini mendarat sebagai null palsu di tiap client
9875
+ // tanpa satu pun error (kelas gagal-senyap ADR-0090/0093/0094/0105). BUKAN DATE_FIELDS —
9876
+ // `at` hidup di dalam JSON-nya, kolomnya sendiri bukan DateTime.
9877
+ spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "sourceHistory", "createdAt", "startedAt", "doneAt", "updatedAt"],
9753
9878
  vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
9754
9879
  sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
9755
9880
  // SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
@@ -19321,6 +19446,148 @@ async function nextSpecId(repoDir) {
19321
19446
  return `SPEC-${maxNum(ids, floor) + 1}`;
19322
19447
  }
19323
19448
 
19449
+ // src/services/spec-fields.ts
19450
+ init_src();
19451
+ function deriveSpecFields(source, payload, manualPriority) {
19452
+ if (source === "goal") {
19453
+ const pick2 = (v) => typeof v === "string" ? v.trim() : "";
19454
+ return {
19455
+ priority: manualPriority,
19456
+ objective: pick2(payload?.goal) || pick2(payload?.done) || "\u2014 goal belum diisi."
19457
+ };
19458
+ }
19459
+ const isQa = source === "qa";
19460
+ const priority = isQa && payload && "severity" in payload ? priorityFromSeverity(payload.severity) : manualPriority;
19461
+ const objective = isQa && payload && "actual" in payload ? payload.actual || payload.steps || "\u2014 audit untuk menelusuri akar masalah." : payload && "outcome" in payload ? payload.outcome || payload.context || "\u2014 brainstorm untuk memperjelas objective." : "";
19462
+ return { priority, objective };
19463
+ }
19464
+
19465
+ // src/services/spec-source.ts
19466
+ init_src();
19467
+ function checkSourceChange(spec, to, payload) {
19468
+ const started2 = spec.stage !== "brainstorming" || spec.baseSha !== null;
19469
+ if (started2) {
19470
+ if (flowForSource(spec.source) !== flowForSource(to))
19471
+ return {
19472
+ ok: false,
19473
+ code: 409,
19474
+ error: "backlog item sudah dimulai \u2014 type hanya bisa pindah ke source dengan flow yang sama"
19475
+ };
19476
+ if (payload !== void 0)
19477
+ return { ok: false, code: 409, error: "backlog item sudah dimulai \u2014 isinya tak bisa diubah" };
19478
+ return { ok: true, payload: spec.payload ?? {}, dropped: [] };
19479
+ }
19480
+ if (payload !== void 0) {
19481
+ if (!payloadMatchesSource(to, payload))
19482
+ return { ok: false, code: 400, error: "bentuk payload tak cocok dengan source" };
19483
+ return { ok: true, payload, dropped: [] };
19484
+ }
19485
+ const c = convertPayload(to, spec.payload);
19486
+ return { ok: true, payload: c.payload, dropped: c.dropped };
19487
+ }
19488
+ function sourceChangeEntry(spec, to, by, at) {
19489
+ return { at: at.toISOString(), from: spec.source, to, by, payload: spec.payload ?? null };
19490
+ }
19491
+ function appendSourceHistory(current, entry) {
19492
+ return [...Array.isArray(current) ? current : [], entry];
19493
+ }
19494
+
19495
+ // src/services/notifications.ts
19496
+ init_db();
19497
+ init_pty();
19498
+ async function recordDrift(vpsId, vpsName, drift, snapshotId) {
19499
+ if (drift.length === 0) return;
19500
+ const ids = drift.map((d) => d.itemId);
19501
+ const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, +${ids.length - 5} lagi` : "");
19502
+ const title = `Drift di "${vpsName}": ${drift.length} item regresi (${shown})`;
19503
+ await prisma.notification.create({
19504
+ data: { type: "drift", key: `drift:${vpsId}:${snapshotId}`, title, projectId: null }
19505
+ }).catch(() => {
19506
+ });
19507
+ }
19508
+ async function recordCompletion(specId, title, projectId) {
19509
+ await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
19510
+ });
19511
+ const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
19512
+ await prisma.notification.create({
19513
+ data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
19514
+ }).catch(() => {
19515
+ });
19516
+ }
19517
+ async function recordFailure(specId, title, projectId, reason) {
19518
+ const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
19519
+ await prisma.notification.create({
19520
+ data: { type: "fail", key: `fail:${specId}`, specId, sessionId: sessionId2, title: `Gagal: ${title} \u2014 ${reason}`, projectId }
19521
+ }).catch(() => {
19522
+ });
19523
+ }
19524
+ async function recordAutoMerge(specId, projectId, title) {
19525
+ const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
19526
+ await prisma.notification.create({
19527
+ data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
19528
+ }).catch(() => {
19529
+ });
19530
+ }
19531
+ async function recordSourceChange(specId, projectId, title, from, to, seq) {
19532
+ const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
19533
+ await prisma.notification.create({
19534
+ data: {
19535
+ type: "spec-source",
19536
+ key: `source:${specId}:${seq}`,
19537
+ specId,
19538
+ sessionId: sessionId2,
19539
+ title: `${specId} \xB7 type ${from} \u2192 ${to} \u2014 ${title}`,
19540
+ projectId
19541
+ }
19542
+ }).catch(() => {
19543
+ });
19544
+ }
19545
+ async function recordNewTicket(ticketId, projectId, projectName, category, title) {
19546
+ const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
19547
+ const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
19548
+ await prisma.notification.create({
19549
+ data: { type: "ticket", key: `ticket:${ticketId}`, projectId, title: t }
19550
+ }).catch(() => {
19551
+ });
19552
+ }
19553
+ async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
19554
+ await prisma.notification.create({
19555
+ data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
19556
+ }).catch(() => {
19557
+ });
19558
+ }
19559
+ var awaiting = /* @__PURE__ */ new Set();
19560
+ async function scanDecisions(read = liveDecisions) {
19561
+ const next = /* @__PURE__ */ new Set();
19562
+ const fresh = [];
19563
+ for (const s2 of read()) {
19564
+ if (!markerFilled(s2.decisionFile)) continue;
19565
+ next.add(s2.id);
19566
+ if (!awaiting.has(s2.id)) fresh.push(s2);
19567
+ }
19568
+ awaiting = next;
19569
+ for (const s2 of fresh) {
19570
+ const title = s2.specId ? (await prisma.spec.findUnique({ where: { id: s2.specId }, select: { title: true } }))?.title ?? s2.specId : s2.id;
19571
+ await prisma.notification.create({
19572
+ data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
19573
+ });
19574
+ }
19575
+ }
19576
+ var DEFAULT_FEED_TAKE = 50;
19577
+ async function notificationsFeed(p = {}) {
19578
+ await scanDecisions();
19579
+ const pageSize = p.limit ? Math.max(1, Math.floor(+p.limit) || 1) : DEFAULT_FEED_TAKE;
19580
+ const page = p.page ? Math.max(1, Math.floor(+p.page) || 1) : 1;
19581
+ const total = await prisma.notification.count();
19582
+ const items = await prisma.notification.findMany({
19583
+ orderBy: { createdAt: "desc" },
19584
+ skip: (page - 1) * pageSize,
19585
+ take: pageSize
19586
+ });
19587
+ const unread = await prisma.notification.count({ where: { readAt: null } });
19588
+ return { items, unread, total, page, pageSize };
19589
+ }
19590
+
19324
19591
  // src/routes/specs.ts
19325
19592
  init_stage_machine();
19326
19593
 
@@ -21978,88 +22245,6 @@ init_pty();
21978
22245
  init_session_phases();
21979
22246
  init_stage_machine();
21980
22247
 
21981
- // src/services/notifications.ts
21982
- init_db();
21983
- init_pty();
21984
- async function recordDrift(vpsId, vpsName, drift, snapshotId) {
21985
- if (drift.length === 0) return;
21986
- const ids = drift.map((d) => d.itemId);
21987
- const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, +${ids.length - 5} lagi` : "");
21988
- const title = `Drift di "${vpsName}": ${drift.length} item regresi (${shown})`;
21989
- await prisma.notification.create({
21990
- data: { type: "drift", key: `drift:${vpsId}:${snapshotId}`, title, projectId: null }
21991
- }).catch(() => {
21992
- });
21993
- }
21994
- async function recordCompletion(specId, title, projectId) {
21995
- await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
21996
- });
21997
- const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
21998
- await prisma.notification.create({
21999
- data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
22000
- }).catch(() => {
22001
- });
22002
- }
22003
- async function recordFailure(specId, title, projectId, reason) {
22004
- const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
22005
- await prisma.notification.create({
22006
- data: { type: "fail", key: `fail:${specId}`, specId, sessionId: sessionId2, title: `Gagal: ${title} \u2014 ${reason}`, projectId }
22007
- }).catch(() => {
22008
- });
22009
- }
22010
- async function recordAutoMerge(specId, projectId, title) {
22011
- const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
22012
- await prisma.notification.create({
22013
- data: { type: "automerge", key: `automerge:${specId}`, specId, sessionId: sessionId2, title, projectId }
22014
- }).catch(() => {
22015
- });
22016
- }
22017
- async function recordNewTicket(ticketId, projectId, projectName, category, title) {
22018
- const short = title.length > 80 ? title.slice(0, 77) + "\u2026" : title;
22019
- const t = `Keluhan baru di "${projectName}": ${category}: ${short}`;
22020
- await prisma.notification.create({
22021
- data: { type: "ticket", key: `ticket:${ticketId}`, projectId, title: t }
22022
- }).catch(() => {
22023
- });
22024
- }
22025
- async function recordLeadDecision(decisionId, title, projectId, specId, sessionId2) {
22026
- await prisma.notification.create({
22027
- data: { type: "lead", key: `lead:${decisionId}`, specId, sessionId: sessionId2, projectId, title }
22028
- }).catch(() => {
22029
- });
22030
- }
22031
- var awaiting = /* @__PURE__ */ new Set();
22032
- async function scanDecisions(read = liveDecisions) {
22033
- const next = /* @__PURE__ */ new Set();
22034
- const fresh = [];
22035
- for (const s2 of read()) {
22036
- if (!markerFilled(s2.decisionFile)) continue;
22037
- next.add(s2.id);
22038
- if (!awaiting.has(s2.id)) fresh.push(s2);
22039
- }
22040
- awaiting = next;
22041
- for (const s2 of fresh) {
22042
- const title = s2.specId ? (await prisma.spec.findUnique({ where: { id: s2.specId }, select: { title: true } }))?.title ?? s2.specId : s2.id;
22043
- await prisma.notification.create({
22044
- data: { type: "decision", specId: s2.specId ?? null, sessionId: s2.id, projectId: s2.projectId || null, title }
22045
- });
22046
- }
22047
- }
22048
- var DEFAULT_FEED_TAKE = 50;
22049
- async function notificationsFeed(p = {}) {
22050
- await scanDecisions();
22051
- const pageSize = p.limit ? Math.max(1, Math.floor(+p.limit) || 1) : DEFAULT_FEED_TAKE;
22052
- const page = p.page ? Math.max(1, Math.floor(+p.page) || 1) : 1;
22053
- const total = await prisma.notification.count();
22054
- const items = await prisma.notification.findMany({
22055
- orderBy: { createdAt: "desc" },
22056
- skip: (page - 1) * pageSize,
22057
- take: pageSize
22058
- });
22059
- const unread = await prisma.notification.count({ where: { readAt: null } });
22060
- return { items, unread, total, page, pageSize };
22061
- }
22062
-
22063
22248
  // src/services/spec-head.ts
22064
22249
  init_db();
22065
22250
  init_src2();
@@ -22110,19 +22295,6 @@ async function liveSpecs(filter = {}) {
22110
22295
 
22111
22296
  // src/routes/specs.ts
22112
22297
  var branchUnknown = async (repoDir, branch) => !(await branchFromCandidates(repoDir)).includes(branch);
22113
- function deriveSpecFields(source, payload, manualPriority) {
22114
- if (source === "goal") {
22115
- const pick2 = (v) => typeof v === "string" ? v.trim() : "";
22116
- return {
22117
- priority: manualPriority,
22118
- objective: pick2(payload?.goal) || pick2(payload?.done) || "\u2014 goal belum diisi."
22119
- };
22120
- }
22121
- const isQa = source === "qa";
22122
- const priority = isQa && payload && "severity" in payload ? payload.severity === "minor" ? "sedang" : "tinggi" : manualPriority;
22123
- const objective = isQa && payload && "actual" in payload ? payload.actual || payload.steps || "\u2014 audit untuk menelusuri akar masalah." : payload && "outcome" in payload ? payload.outcome || payload.context || "\u2014 brainstorm untuk memperjelas objective." : "";
22124
- return { priority, objective };
22125
- }
22126
22298
  function filterSpecs(specs, f) {
22127
22299
  const needle = (f.q ?? "").trim().toLowerCase();
22128
22300
  const from = dayStart(f.from);
@@ -22275,6 +22447,40 @@ ${item.context}` : item.context;
22275
22447
  await notifySynced("spec", id);
22276
22448
  return updated;
22277
22449
  });
22450
+ app2.post("/specs/:id/source", async (req, reply) => {
22451
+ const { id } = req.params;
22452
+ const parsed = zChangeSpecSource.safeParse(req.body);
22453
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
22454
+ const spec = await prisma.spec.findUnique({ where: { id } });
22455
+ if (!spec) return reply.code(404).send({ error: "not found" });
22456
+ const to = parsed.data.source;
22457
+ if (to === spec.source) return reply.code(400).send({ error: "source tak berubah" });
22458
+ const gate = checkSourceChange(spec, to, parsed.data.payload);
22459
+ if (!gate.ok) return reply.code(gate.code).send({ error: gate.error });
22460
+ const by = req.user?.email ?? "system";
22461
+ const history = appendSourceHistory(
22462
+ spec.sourceHistory,
22463
+ sourceChangeEntry(spec, to, by, /* @__PURE__ */ new Date())
22464
+ );
22465
+ const { priority, objective } = deriveSpecFields(
22466
+ to,
22467
+ gate.payload,
22468
+ gate.payload.priority ?? spec.priority
22469
+ );
22470
+ const updated = await prisma.spec.update({
22471
+ where: { id },
22472
+ data: {
22473
+ source: to,
22474
+ payload: gate.payload,
22475
+ priority,
22476
+ objective,
22477
+ sourceHistory: history
22478
+ }
22479
+ });
22480
+ await recordSourceChange(spec.id, spec.projectId, spec.title, spec.source, to, history.length);
22481
+ await notifySynced("spec", id);
22482
+ return updated;
22483
+ });
22278
22484
  app2.get("/specs/:id/docs", async (req) => ({ files: await listSpecDocs(req.params.id) }));
22279
22485
  app2.get("/specs/:id/docs/*", async (req, reply) => {
22280
22486
  const { id } = req.params;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanoman",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Orchestrator + dashboard workflow docs-driven untuk sesi Claude Code / Codex",
5
5
  "type": "module",
6
6
  "repository": {