hanoman 0.1.40 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +21 -3
- package/dist/server.js +131 -44
- package/docs/agent-integration.md +5 -1
- package/package.json +1 -1
- package/prisma/migrations/20260815130000_spec_manual_done/migration.sql +9 -0
- package/prisma/schema.prisma +6 -0
- package/web/assets/{index-zZ7g98st.js → index-BLt4wnhB.js} +1539 -1539
- package/web/index.html +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4411,7 +4411,7 @@ var init_auto_merge = __esm({
|
|
|
4411
4411
|
});
|
|
4412
4412
|
|
|
4413
4413
|
// ../shared/src/entities.ts
|
|
4414
|
-
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;
|
|
4414
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, 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;
|
|
4415
4415
|
var init_entities = __esm({
|
|
4416
4416
|
"../shared/src/entities.ts"() {
|
|
4417
4417
|
"use strict";
|
|
@@ -4469,6 +4469,11 @@ var init_entities = __esm({
|
|
|
4469
4469
|
by: external_exports.string(),
|
|
4470
4470
|
payload: external_exports.unknown().optional()
|
|
4471
4471
|
});
|
|
4472
|
+
zManualDone = external_exports.object({
|
|
4473
|
+
at: external_exports.string(),
|
|
4474
|
+
by: external_exports.string(),
|
|
4475
|
+
reason: external_exports.string().optional()
|
|
4476
|
+
});
|
|
4472
4477
|
zSpec = external_exports.object({
|
|
4473
4478
|
id: external_exports.string(),
|
|
4474
4479
|
projectId: external_exports.string(),
|
|
@@ -4500,7 +4505,10 @@ var init_entities = __esm({
|
|
|
4500
4505
|
// SPEC-546 · ADR-0109 · jejak konversi type. `.default([])` menjaga respons/klien versi lama
|
|
4501
4506
|
// tetap parse; kolom DB-nya `Json?` sehingga baris yang belum pernah dikonversi mengirim
|
|
4502
4507
|
// `null` — pemakai UI menulis `spec.sourceHistory ?? []`, cermin `blockedBy`.
|
|
4503
|
-
sourceHistory: external_exports.array(zSourceChange).default([])
|
|
4508
|
+
sourceHistory: external_exports.array(zSourceChange).default([]),
|
|
4509
|
+
// SPEC-804 · ADR-0120 · terisi hanya bila item ditandai selesai manual; null = selesai lewat
|
|
4510
|
+
// sesi atau belum selesai. `.nullable().default(null)` menjaga respons/klien versi lama parse.
|
|
4511
|
+
manualDone: zManualDone.nullable().default(null)
|
|
4504
4512
|
});
|
|
4505
4513
|
NOTIFY_SOUNDS = [
|
|
4506
4514
|
"off",
|
|
@@ -5008,7 +5016,7 @@ var init_cron_expr = __esm({
|
|
|
5008
5016
|
});
|
|
5009
5017
|
|
|
5010
5018
|
// ../shared/src/dto.ts
|
|
5011
|
-
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, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5019
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, 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, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5012
5020
|
var init_dto = __esm({
|
|
5013
5021
|
"../shared/src/dto.ts"() {
|
|
5014
5022
|
"use strict";
|
|
@@ -5115,6 +5123,10 @@ var init_dto = __esm({
|
|
|
5115
5123
|
if (o.payload !== void 0 && !payloadMatchesSource(o.source, o.payload))
|
|
5116
5124
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
|
|
5117
5125
|
});
|
|
5126
|
+
zMarkSpecDone = external_exports.object({
|
|
5127
|
+
reason: external_exports.string().trim().max(280).optional(),
|
|
5128
|
+
confirm: external_exports.boolean().optional()
|
|
5129
|
+
});
|
|
5118
5130
|
zIntegrate = external_exports.object({
|
|
5119
5131
|
op: external_exports.enum(["merge", "rebase"]),
|
|
5120
5132
|
target: external_exports.string().regex(/^(local|origin):.+/)
|
|
@@ -5586,6 +5598,8 @@ var init_api = __esm({
|
|
|
5586
5598
|
specIntegrate: (id) => `${API}/specs/${id}/integrate`,
|
|
5587
5599
|
// SPEC-546 · ADR-0109 · ubah type/source item in-place (operasi khusus, bukan field PATCH).
|
|
5588
5600
|
specSource: (id) => `${API}/specs/${id}/source`,
|
|
5601
|
+
// SPEC-804 · ADR-0120 · tandai item selesai manual (operasi khusus, bukan field PATCH).
|
|
5602
|
+
specDone: (id) => `${API}/specs/${id}/done`,
|
|
5589
5603
|
specReview: (id) => `${API}/specs/${id}/review`,
|
|
5590
5604
|
specReviewFile: (id, path) => `${API}/specs/${id}/review/${path}`,
|
|
5591
5605
|
settings: `${API}/settings`,
|
|
@@ -6152,6 +6166,9 @@ var init_webhook = __esm({
|
|
|
6152
6166
|
"autoMerge",
|
|
6153
6167
|
"createdAt",
|
|
6154
6168
|
"startedAt",
|
|
6169
|
+
// SPEC-804 · ADR-0120 · penerima harus bisa membedakan "selesai lewat sesi" dari "ditandai
|
|
6170
|
+
// manusia" tanpa mendiff dua amplop. Peristiwanya tetap `spec.stage_changed`.
|
|
6171
|
+
"manualDone",
|
|
6155
6172
|
"updatedAt"
|
|
6156
6173
|
],
|
|
6157
6174
|
projectIdField: "projectId",
|
|
@@ -6190,6 +6207,7 @@ var init_webhook = __esm({
|
|
|
6190
6207
|
autoMerge: null,
|
|
6191
6208
|
createdAt: "2026-08-01T02:10:00.000Z",
|
|
6192
6209
|
startedAt: "2026-08-01T02:12:31.000Z",
|
|
6210
|
+
manualDone: null,
|
|
6193
6211
|
updatedAt: "2026-08-01T09:41:22.000Z"
|
|
6194
6212
|
}
|
|
6195
6213
|
},
|
package/dist/server.js
CHANGED
|
@@ -4557,7 +4557,7 @@ function cmpVersion(a, b) {
|
|
|
4557
4557
|
}
|
|
4558
4558
|
return 0;
|
|
4559
4559
|
}
|
|
4560
|
-
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;
|
|
4560
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSourceChange, zManualDone, 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;
|
|
4561
4561
|
var init_entities = __esm({
|
|
4562
4562
|
"../shared/src/entities.ts"() {
|
|
4563
4563
|
"use strict";
|
|
@@ -4615,6 +4615,11 @@ var init_entities = __esm({
|
|
|
4615
4615
|
by: external_exports.string(),
|
|
4616
4616
|
payload: external_exports.unknown().optional()
|
|
4617
4617
|
});
|
|
4618
|
+
zManualDone = external_exports.object({
|
|
4619
|
+
at: external_exports.string(),
|
|
4620
|
+
by: external_exports.string(),
|
|
4621
|
+
reason: external_exports.string().optional()
|
|
4622
|
+
});
|
|
4618
4623
|
zSpec = external_exports.object({
|
|
4619
4624
|
id: external_exports.string(),
|
|
4620
4625
|
projectId: external_exports.string(),
|
|
@@ -4646,7 +4651,10 @@ var init_entities = __esm({
|
|
|
4646
4651
|
// SPEC-546 · ADR-0109 · jejak konversi type. `.default([])` menjaga respons/klien versi lama
|
|
4647
4652
|
// tetap parse; kolom DB-nya `Json?` sehingga baris yang belum pernah dikonversi mengirim
|
|
4648
4653
|
// `null` — pemakai UI menulis `spec.sourceHistory ?? []`, cermin `blockedBy`.
|
|
4649
|
-
sourceHistory: external_exports.array(zSourceChange).default([])
|
|
4654
|
+
sourceHistory: external_exports.array(zSourceChange).default([]),
|
|
4655
|
+
// SPEC-804 · ADR-0120 · terisi hanya bila item ditandai selesai manual; null = selesai lewat
|
|
4656
|
+
// sesi atau belum selesai. `.nullable().default(null)` menjaga respons/klien versi lama parse.
|
|
4657
|
+
manualDone: zManualDone.nullable().default(null)
|
|
4650
4658
|
});
|
|
4651
4659
|
NOTIFY_SOUNDS = [
|
|
4652
4660
|
"off",
|
|
@@ -5450,7 +5458,7 @@ var init_cron_expr = __esm({
|
|
|
5450
5458
|
function flowForSource(source) {
|
|
5451
5459
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
5452
5460
|
}
|
|
5453
|
-
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, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5461
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zMarkSpecDone, 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, USER_ROLES, zUserRole, zLogin, zSignup, zSetup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus, zCronExpr, zCreateCron, zPatchCron, zSchedulerCron, zSchedulerCronRun;
|
|
5454
5462
|
var init_dto = __esm({
|
|
5455
5463
|
"../shared/src/dto.ts"() {
|
|
5456
5464
|
"use strict";
|
|
@@ -5557,6 +5565,10 @@ var init_dto = __esm({
|
|
|
5557
5565
|
if (o.payload !== void 0 && !payloadMatchesSource(o.source, o.payload))
|
|
5558
5566
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["payload"], message: "bentuk payload tak cocok dengan source" });
|
|
5559
5567
|
});
|
|
5568
|
+
zMarkSpecDone = external_exports.object({
|
|
5569
|
+
reason: external_exports.string().trim().max(280).optional(),
|
|
5570
|
+
confirm: external_exports.boolean().optional()
|
|
5571
|
+
});
|
|
5560
5572
|
zIntegrate = external_exports.object({
|
|
5561
5573
|
op: external_exports.enum(["merge", "rebase"]),
|
|
5562
5574
|
target: external_exports.string().regex(/^(local|origin):.+/)
|
|
@@ -6028,6 +6040,8 @@ var init_api = __esm({
|
|
|
6028
6040
|
specIntegrate: (id) => `${API}/specs/${id}/integrate`,
|
|
6029
6041
|
// SPEC-546 · ADR-0109 · ubah type/source item in-place (operasi khusus, bukan field PATCH).
|
|
6030
6042
|
specSource: (id) => `${API}/specs/${id}/source`,
|
|
6043
|
+
// SPEC-804 · ADR-0120 · tandai item selesai manual (operasi khusus, bukan field PATCH).
|
|
6044
|
+
specDone: (id) => `${API}/specs/${id}/done`,
|
|
6031
6045
|
specReview: (id) => `${API}/specs/${id}/review`,
|
|
6032
6046
|
specReviewFile: (id, path2) => `${API}/specs/${id}/review/${path2}`,
|
|
6033
6047
|
settings: `${API}/settings`,
|
|
@@ -6749,6 +6763,9 @@ var init_webhook = __esm({
|
|
|
6749
6763
|
"autoMerge",
|
|
6750
6764
|
"createdAt",
|
|
6751
6765
|
"startedAt",
|
|
6766
|
+
// SPEC-804 · ADR-0120 · penerima harus bisa membedakan "selesai lewat sesi" dari "ditandai
|
|
6767
|
+
// manusia" tanpa mendiff dua amplop. Peristiwanya tetap `spec.stage_changed`.
|
|
6768
|
+
"manualDone",
|
|
6752
6769
|
"updatedAt"
|
|
6753
6770
|
],
|
|
6754
6771
|
projectIdField: "projectId",
|
|
@@ -6787,6 +6804,7 @@ var init_webhook = __esm({
|
|
|
6787
6804
|
autoMerge: null,
|
|
6788
6805
|
createdAt: "2026-08-01T02:10:00.000Z",
|
|
6789
6806
|
startedAt: "2026-08-01T02:12:31.000Z",
|
|
6807
|
+
manualDone: null,
|
|
6790
6808
|
updatedAt: "2026-08-01T09:41:22.000Z"
|
|
6791
6809
|
}
|
|
6792
6810
|
},
|
|
@@ -9738,8 +9756,8 @@ function assertRuntimeBoundary(env, runtime) {
|
|
|
9738
9756
|
if (runtime.uid === 0) throw new Error("production Hanoman harus berjalan sebagai user non-root");
|
|
9739
9757
|
if (env.HANOMAN_SESSION_SANDBOX !== "podman")
|
|
9740
9758
|
throw new Error("HANOMAN_SESSION_SANDBOX=podman wajib di production");
|
|
9741
|
-
if (!env.
|
|
9742
|
-
throw new Error("public/control origin wajib dipisahkan di production");
|
|
9759
|
+
if (!env.HANOMAN_CONTROL_ORIGINS || !env.HANOMAN_PUBLIC_ORIGINS && !env.HANOMAN_SINGLE_ORIGIN)
|
|
9760
|
+
throw new Error("public/control origin wajib dipisahkan di production (HANOMAN_SINGLE_ORIGIN=1 untuk sadar memilih satu origin)");
|
|
9743
9761
|
if (!env.HANOMAN_TRUST_PROXY) throw new Error("trusted proxy hop/CIDR wajib di production");
|
|
9744
9762
|
if (runtime.host !== "127.0.0.1" && runtime.host !== "::1" && runtime.host !== "localhost")
|
|
9745
9763
|
throw new Error("origin Hanoman harus bind loopback; buka hanya reverse proxy");
|
|
@@ -10531,6 +10549,7 @@ __export(sync_exports, {
|
|
|
10531
10549
|
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
10532
10550
|
__FIELDS: () => __FIELDS,
|
|
10533
10551
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
10552
|
+
__JSON_FIELDS: () => __JSON_FIELDS,
|
|
10534
10553
|
applyPush: () => applyPush,
|
|
10535
10554
|
backfillFeed: () => backfillFeed,
|
|
10536
10555
|
consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
|
|
@@ -10774,7 +10793,7 @@ async function upsertLocal(entity, id, version2, data) {
|
|
|
10774
10793
|
function setAcceptedHook(hook) {
|
|
10775
10794
|
onAccepted = hook;
|
|
10776
10795
|
}
|
|
10777
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10796
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10778
10797
|
var init_sync = __esm({
|
|
10779
10798
|
"src/services/sync.ts"() {
|
|
10780
10799
|
"use strict";
|
|
@@ -10806,7 +10825,10 @@ var init_sync = __esm({
|
|
|
10806
10825
|
// TETAP berhasil, jadi kolom yang terlewat di sini mendarat sebagai null palsu di tiap client
|
|
10807
10826
|
// tanpa satu pun error (kelas gagal-senyap ADR-0090/0093/0094/0105). BUKAN DATE_FIELDS —
|
|
10808
10827
|
// `at` hidup di dalam JSON-nya, kolomnya sendiri bukan DateTime.
|
|
10809
|
-
|
|
10828
|
+
// SPEC-804 · ADR-0120 · manualDone ikut menyeberang: "item ini ditandai selesai manusia" adalah
|
|
10829
|
+
// bagian keadaan yang harus dilihat sama oleh semua mesin — di antaranya gerbang auto-merge.
|
|
10830
|
+
// BUKAN DATE_FIELDS, alasan yang sama dengan sourceHistory.
|
|
10831
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "sourceHistory", "manualDone", "createdAt", "startedAt", "doneAt", "updatedAt"],
|
|
10810
10832
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
10811
10833
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
10812
10834
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
@@ -10874,12 +10896,14 @@ var init_sync = __esm({
|
|
|
10874
10896
|
"spec:payload",
|
|
10875
10897
|
"spec:dependsOn",
|
|
10876
10898
|
"spec:sourceHistory",
|
|
10899
|
+
"spec:manualDone",
|
|
10877
10900
|
"vps:health",
|
|
10878
10901
|
"vps:audit",
|
|
10879
10902
|
"customAgent:tools",
|
|
10880
10903
|
"customAgent:mentions",
|
|
10881
10904
|
"githubIssue:labels"
|
|
10882
10905
|
]);
|
|
10906
|
+
__JSON_FIELDS = JSON_FIELDS;
|
|
10883
10907
|
__FIELDS_FOR_TEST = FIELDS;
|
|
10884
10908
|
}
|
|
10885
10909
|
});
|
|
@@ -25773,6 +25797,65 @@ function appendSourceHistory(current, entry) {
|
|
|
25773
25797
|
|
|
25774
25798
|
// src/routes/specs.ts
|
|
25775
25799
|
init_notifications();
|
|
25800
|
+
|
|
25801
|
+
// src/services/spec-complete.ts
|
|
25802
|
+
init_db();
|
|
25803
|
+
init_notifications();
|
|
25804
|
+
|
|
25805
|
+
// src/services/session-result.ts
|
|
25806
|
+
init_db();
|
|
25807
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
25808
|
+
var WHITELIST = [
|
|
25809
|
+
"projectId",
|
|
25810
|
+
"specId",
|
|
25811
|
+
"oldStage",
|
|
25812
|
+
"newStage",
|
|
25813
|
+
"commitSha",
|
|
25814
|
+
"branch",
|
|
25815
|
+
"prUrl",
|
|
25816
|
+
"status",
|
|
25817
|
+
"deviceId",
|
|
25818
|
+
"author"
|
|
25819
|
+
];
|
|
25820
|
+
async function recordSessionResult(input) {
|
|
25821
|
+
const id = randomUUID3();
|
|
25822
|
+
const data = { id };
|
|
25823
|
+
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
25824
|
+
if (data.status === void 0) data.status = "done";
|
|
25825
|
+
if (data.projectId === void 0) throw new Error("session result butuh projectId");
|
|
25826
|
+
await prisma.sessionResult.create({ data });
|
|
25827
|
+
await notifySynced("sessionResult", id);
|
|
25828
|
+
return { id };
|
|
25829
|
+
}
|
|
25830
|
+
|
|
25831
|
+
// src/services/spec-complete.ts
|
|
25832
|
+
async function completeSpecManually(spec, input) {
|
|
25833
|
+
const at = input.at ?? /* @__PURE__ */ new Date();
|
|
25834
|
+
const manualDone = {
|
|
25835
|
+
at: at.toISOString(),
|
|
25836
|
+
by: input.by,
|
|
25837
|
+
...input.reason ? { reason: input.reason } : {}
|
|
25838
|
+
};
|
|
25839
|
+
const { count } = await prisma.spec.updateMany({
|
|
25840
|
+
where: { id: spec.id, stage: { not: "done" } },
|
|
25841
|
+
data: { stage: "done", manualDone }
|
|
25842
|
+
});
|
|
25843
|
+
if (count === 0) return { ok: false };
|
|
25844
|
+
await recordCompletion(spec.id, spec.title, spec.projectId);
|
|
25845
|
+
await recordSessionResult({
|
|
25846
|
+
projectId: spec.projectId,
|
|
25847
|
+
specId: spec.id,
|
|
25848
|
+
oldStage: spec.stage,
|
|
25849
|
+
newStage: "done",
|
|
25850
|
+
status: "done",
|
|
25851
|
+
author: input.by
|
|
25852
|
+
}).catch(() => {
|
|
25853
|
+
});
|
|
25854
|
+
await notifySynced("spec", spec.id);
|
|
25855
|
+
return { ok: true, spec: await prisma.spec.findUnique({ where: { id: spec.id } }) };
|
|
25856
|
+
}
|
|
25857
|
+
|
|
25858
|
+
// src/routes/specs.ts
|
|
25776
25859
|
init_stage_machine();
|
|
25777
25860
|
|
|
25778
25861
|
// src/services/stage-artifacts.ts
|
|
@@ -28693,6 +28776,23 @@ ${item.context}` : item.context;
|
|
|
28693
28776
|
await notifySynced("spec", id);
|
|
28694
28777
|
return updated;
|
|
28695
28778
|
});
|
|
28779
|
+
app2.post("/specs/:id/done", async (req, reply) => {
|
|
28780
|
+
const { id } = req.params;
|
|
28781
|
+
const parsed = zMarkSpecDone.safeParse(req.body ?? {});
|
|
28782
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
28783
|
+
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
28784
|
+
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
28785
|
+
if (spec.stage === "done") return reply.code(409).send({ error: "backlog item sudah selesai" });
|
|
28786
|
+
const live = listSessions().find((s2) => s2.specId === id && !s2.exited);
|
|
28787
|
+
if (live && parsed.data.confirm !== true)
|
|
28788
|
+
return reply.code(409).send({ error: "confirm-required", session: { id: live.id, agent: live.agent } });
|
|
28789
|
+
const res = await completeSpecManually(spec, {
|
|
28790
|
+
by: req.user?.email ?? "system",
|
|
28791
|
+
reason: parsed.data.reason || void 0
|
|
28792
|
+
});
|
|
28793
|
+
if (!res.ok) return reply.code(409).send({ error: "backlog item sudah selesai" });
|
|
28794
|
+
return res.spec;
|
|
28795
|
+
});
|
|
28696
28796
|
app2.get("/specs/:id/docs", async (req) => ({ files: await listSpecDocs(req.params.id) }));
|
|
28697
28797
|
app2.get("/specs/:id/docs/*", async (req, reply) => {
|
|
28698
28798
|
const { id } = req.params;
|
|
@@ -30520,32 +30620,6 @@ function startWorktreeReaper(deps = prodReaperDeps) {
|
|
|
30520
30620
|
timer.unref();
|
|
30521
30621
|
}
|
|
30522
30622
|
|
|
30523
|
-
// src/services/session-result.ts
|
|
30524
|
-
init_db();
|
|
30525
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
30526
|
-
var WHITELIST = [
|
|
30527
|
-
"projectId",
|
|
30528
|
-
"specId",
|
|
30529
|
-
"oldStage",
|
|
30530
|
-
"newStage",
|
|
30531
|
-
"commitSha",
|
|
30532
|
-
"branch",
|
|
30533
|
-
"prUrl",
|
|
30534
|
-
"status",
|
|
30535
|
-
"deviceId",
|
|
30536
|
-
"author"
|
|
30537
|
-
];
|
|
30538
|
-
async function recordSessionResult(input) {
|
|
30539
|
-
const id = randomUUID3();
|
|
30540
|
-
const data = { id };
|
|
30541
|
-
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
30542
|
-
if (data.status === void 0) data.status = "done";
|
|
30543
|
-
if (data.projectId === void 0) throw new Error("session result butuh projectId");
|
|
30544
|
-
await prisma.sessionResult.create({ data });
|
|
30545
|
-
await notifySynced("sessionResult", id);
|
|
30546
|
-
return { id };
|
|
30547
|
-
}
|
|
30548
|
-
|
|
30549
30623
|
// src/routes/terminal.ts
|
|
30550
30624
|
init_notifications();
|
|
30551
30625
|
init_stage_machine();
|
|
@@ -40487,9 +40561,9 @@ async function help_default(app2) {
|
|
|
40487
40561
|
return reply.code(201).send({ number: ticket.number, key, statusPath });
|
|
40488
40562
|
});
|
|
40489
40563
|
app2.get("/help/:slug/tickets/:key", async (req, reply) => {
|
|
40490
|
-
const {
|
|
40564
|
+
const { key } = req.params;
|
|
40491
40565
|
const t = await prisma.ticket.findFirst({
|
|
40492
|
-
where: {
|
|
40566
|
+
where: { OR: [{ accessKeyHash: hashAccessKey(key) }, { shareToken: key }] }
|
|
40493
40567
|
});
|
|
40494
40568
|
if (!t) return reply.code(404).send({ error: "not found" });
|
|
40495
40569
|
let stage = null;
|
|
@@ -40594,7 +40668,7 @@ var view6 = (t) => ({
|
|
|
40594
40668
|
attachmentCount: t._count?.attachments ?? 0,
|
|
40595
40669
|
createdAt: t.createdAt.toISOString()
|
|
40596
40670
|
});
|
|
40597
|
-
async function tickets_default(app2) {
|
|
40671
|
+
async function tickets_default(app2, opts = {}) {
|
|
40598
40672
|
app2.get("/tickets", async (req) => {
|
|
40599
40673
|
const { project, status, q, page, limit } = req.query;
|
|
40600
40674
|
const where = {};
|
|
@@ -40625,7 +40699,7 @@ async function tickets_default(app2) {
|
|
|
40625
40699
|
shareToken = generateShareToken();
|
|
40626
40700
|
await prisma.ticket.update({ where: { id }, data: { shareToken } });
|
|
40627
40701
|
}
|
|
40628
|
-
const base2 = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40702
|
+
const base2 = opts.publicBase ?? `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40629
40703
|
const publicStatusUrl = `${base2}/help/${encodeURIComponent(t.projectId)}/status/${shareToken}`;
|
|
40630
40704
|
return {
|
|
40631
40705
|
...view6(t),
|
|
@@ -45143,22 +45217,31 @@ init_runtime();
|
|
|
45143
45217
|
|
|
45144
45218
|
// src/services/ingress-policy.ts
|
|
45145
45219
|
function origins(value, key) {
|
|
45146
|
-
const out3 =
|
|
45220
|
+
const out3 = [];
|
|
45147
45221
|
for (const raw2 of value?.split(",") ?? []) {
|
|
45148
45222
|
const input = raw2.trim();
|
|
45149
45223
|
if (!input) continue;
|
|
45150
45224
|
const parsed = new URL(input);
|
|
45151
45225
|
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password || parsed.pathname !== "/")
|
|
45152
45226
|
throw new Error(`${key} harus berisi exact http(s) origin`);
|
|
45153
|
-
out3.
|
|
45227
|
+
out3.push({ host: parsed.host.toLowerCase(), base: `${parsed.protocol}//${parsed.host}` });
|
|
45154
45228
|
}
|
|
45155
45229
|
return out3;
|
|
45156
45230
|
}
|
|
45157
45231
|
function loadIngressPolicy(env) {
|
|
45158
|
-
const
|
|
45159
|
-
const
|
|
45232
|
+
const pub = origins(env.HANOMAN_PUBLIC_ORIGINS, "HANOMAN_PUBLIC_ORIGINS");
|
|
45233
|
+
const control = origins(env.HANOMAN_CONTROL_ORIGINS, "HANOMAN_CONTROL_ORIGINS");
|
|
45234
|
+
const publicHosts = new Set(pub.map((o) => o.host));
|
|
45235
|
+
const controlHosts = new Set(control.map((o) => o.host));
|
|
45160
45236
|
for (const host2 of publicHosts) if (controlHosts.has(host2)) throw new Error("public dan control origin harus berbeda");
|
|
45161
|
-
return {
|
|
45237
|
+
return {
|
|
45238
|
+
publicHosts,
|
|
45239
|
+
controlHosts,
|
|
45240
|
+
// SPEC-805 · origin publik pertama, lengkap dengan scheme-nya: satu-satunya basis sah untuk
|
|
45241
|
+
// link status yang dibagikan operator, yang selalu lahir di request ber-host control.
|
|
45242
|
+
publicBase: pub[0]?.base ?? null,
|
|
45243
|
+
enforce: publicHosts.size > 0 || controlHosts.size > 0
|
|
45244
|
+
};
|
|
45162
45245
|
}
|
|
45163
45246
|
function publicPath(method, path2) {
|
|
45164
45247
|
if (method === "GET" && path2 === "/api/health") return true;
|
|
@@ -45170,7 +45253,8 @@ function classifyIngress(req, policy) {
|
|
|
45170
45253
|
const host2 = req.host.trim().toLowerCase();
|
|
45171
45254
|
const path2 = req.url.split("?")[0] ?? req.url;
|
|
45172
45255
|
if (policy.publicHosts.has(host2)) return publicPath(req.method, path2) ? "public" : "denied";
|
|
45173
|
-
if (policy.controlHosts.has(host2))
|
|
45256
|
+
if (policy.controlHosts.has(host2))
|
|
45257
|
+
return policy.publicBase && path2.startsWith("/api/help") ? "denied" : "control";
|
|
45174
45258
|
return "denied";
|
|
45175
45259
|
}
|
|
45176
45260
|
function trustProxyFromEnv(env) {
|
|
@@ -45203,6 +45287,8 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45203
45287
|
app2.addHook("onRequest", async (req, reply) => {
|
|
45204
45288
|
const role = classifyIngress({ host: req.headers.host ?? "", method: req.method, url: req.url }, ingress);
|
|
45205
45289
|
if (role === "denied") return reply.code(404).send({ error: "not found" });
|
|
45290
|
+
if (role === "control" && ingress.publicBase && req.url.startsWith("/help/"))
|
|
45291
|
+
return reply.redirect(`${ingress.publicBase}${req.url}`, 302);
|
|
45206
45292
|
});
|
|
45207
45293
|
app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
45208
45294
|
if (!body) return done(null, void 0);
|
|
@@ -45282,7 +45368,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45282
45368
|
await api.register(session_history_default);
|
|
45283
45369
|
await api.register(config_default);
|
|
45284
45370
|
await api.register(help_default);
|
|
45285
|
-
await api.register(tickets_default);
|
|
45371
|
+
await api.register(tickets_default, { publicBase: ingress.publicBase });
|
|
45286
45372
|
await api.register(scheduler_default);
|
|
45287
45373
|
await api.register(codex);
|
|
45288
45374
|
await api.register(methods);
|
|
@@ -45975,6 +46061,7 @@ async function sweepAutoMerge(deps = prodAutoMergeDeps, now = /* @__PURE__ */ ne
|
|
|
45975
46061
|
async function settleOne(c, deps, now) {
|
|
45976
46062
|
const spec = await prisma.spec.findUnique({ where: { id: c.specId } });
|
|
45977
46063
|
if (!spec || spec.stage !== "done") return false;
|
|
46064
|
+
if (spec.manualDone) return false;
|
|
45978
46065
|
const project = await prisma.project.findUnique({ where: { id: spec.projectId } });
|
|
45979
46066
|
if (!project) return false;
|
|
45980
46067
|
const policy = resolveAutoMerge(
|
|
@@ -168,6 +168,7 @@ dan tak memakai agent token.
|
|
|
168
168
|
| `GET /api/specs` | `backlog:read` | backlog. Filter: `project`, `source`, `q`, `stage`, `priority`, `startable=true`, `dateField=created\|started` + `from`/`to` (`YYYY-MM-DD`, inklusif), `page`, `limit`. |
|
|
169
169
|
| `POST /api/specs` | `backlog:write` | buat backlog item — bentuk payload di §7. |
|
|
170
170
|
| `PATCH /api/specs/:id` | `backlog:write` | ubah item; konten hanya selagi belum dimulai. |
|
|
171
|
+
| `POST /api/specs/:id/done` | `backlog:write` | tandai item **selesai** tanpa menjalankan sesi — untuk pekerjaan yang beres di luar hanoman. Body `{ reason?: string (≤280), confirm?: boolean }`, keduanya opsional; balasannya `Spec` yang sudah `stage:"done"`. Tak menjalankan maupun menghentikan sesi apa pun. |
|
|
171
172
|
| `GET /api/specs/:id/docs` | `backlog:read` | dokumen yang ditulis sesi item itu. |
|
|
172
173
|
| `GET /api/specs/:id/review` | `backlog:read` | diff hasil kerja sesi. |
|
|
173
174
|
| `GET /api/projects/:id/docs` | `docs:read` | index Source of Truth project. |
|
|
@@ -271,7 +272,9 @@ capability-nya. Capability menjawab "boleh?", bukan "sebaiknya?".
|
|
|
271
272
|
| `POST /api/lead/decisions` | putusannya bisa **menggerakkan sesi** (integrate ke main, menghentikan sesi) dan selalu melahirkan baris jejak permanen (ADR-0091/0098). |
|
|
272
273
|
|
|
273
274
|
Perlakukan `POST /api/specs/:id/integrate`, `DELETE /api/specs/:id`, dan perubahan `stage` dengan
|
|
274
|
-
disiplin yang sama: ketiganya mengubah sejarah git atau membuang pekerjaan.
|
|
275
|
+
disiplin yang sama: ketiganya mengubah sejarah git atau membuang pekerjaan. `POST /api/specs/:id/done`
|
|
276
|
+
tak menyentuh git, tetapi ia **menyatakan pekerjaan orang lain selesai** — pakai hanya bila kamu punya
|
|
277
|
+
buktinya, dan tulis buktinya di `reason`.
|
|
275
278
|
|
|
276
279
|
**Preseden yang mengikat:** MCP server resmi (`hanoman mcp`, §13) sengaja **tak punya tool** untuk
|
|
277
280
|
satu pun dari yang di atas — batasnya ada di katalog tool, bukan di token. Token yang punya
|
|
@@ -286,6 +289,7 @@ memanggilnya; jangan lakukan tanpa manusia.
|
|
|
286
289
|
| `q` mencari di `id`, `title`, dan `objective` saja — ia **tak menyentuh `payload`** | untuk mencari isi brief/QA, ambil itemnya lalu baca `payload` sendiri |
|
|
287
290
|
| `id` dan `stage` yang kamu sertakan di `POST /api/specs` **dibuang diam-diam** — tak ada galat | `id` diterbitkan server (`SPEC-nnn` berikutnya), `stage` selalu mulai `brainstorming`. Untuk mengubah stage pakai `PATCH /api/specs/:id`, dan ia hanya boleh **mundur** (ADR-0027) |
|
|
288
291
|
| **`GET /api/specs/:id` tidak ada** | `GET /api/specs?q=SPEC-489` lalu cocokkan `id` **persis** — `q` itu substring, jadi ia bisa mengembalikan lebih dari satu |
|
|
292
|
+
| `POST /api/specs/:id/done` menjawab **409 `{"error":"confirm-required","session":{...}}`** — itu bukan penolakan, melainkan pemberitahuan bahwa masih ada **sesi hidup** untuk item itu | putuskan dulu apakah sesi itu memang sudah tak relevan; bila ya, kirim ulang dengan `{"confirm": true}`. Sesinya **tidak** ikut dihentikan — tutup sesinya sendiri bila perlu. 409 `{"error":"backlog item sudah selesai"}` berarti item itu memang sudah `done` |
|
|
289
293
|
| daftar mengembalikan amplop `{ items, total, page, pageSize }` | jangan perlakukan responsnya sebagai array |
|
|
290
294
|
| tanpa `limit`, daftar mengembalikan **seluruh** item dalam satu halaman | kirim `limit` untuk backlog besar |
|
|
291
295
|
| **`GET /api/notifications` adalah pengecualiannya**: tanpa `limit` ia mengembalikan **50 teratas**, bukan seluruhnya — angka penuhnya ada di `total` | kirim `page`/`limit` bila kamu butuh riwayat lama; jangan simpulkan `items.length` = seluruh notifikasi |
|
package/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- SPEC-804 · ADR-0120 · jejak penandaan selesai manual sebagai kolom.
|
|
2
|
+
--
|
|
3
|
+
-- Ditulis tangan (bukan `migrate dev`): worktree tetangga membuat `migrate dev` me-reset DB saat
|
|
4
|
+
-- ada drift. ADITIF murni — satu kolom NULLABLE tanpa default, tak ada tabel diredefinisi.
|
|
5
|
+
--
|
|
6
|
+
-- TANPA backfill, sengaja: sebelum spec ini jalur "tandai selesai manual" memang tak ada, jadi
|
|
7
|
+
-- tak ada stempel lama yang bisa dipulihkan. Item lama tetap NULL = "selesai lewat sesi / tak
|
|
8
|
+
-- diketahui", dan itu jawaban yang jujur.
|
|
9
|
+
ALTER TABLE "Spec" ADD COLUMN "manualDone" JSONB;
|
package/prisma/schema.prisma
CHANGED
|
@@ -63,6 +63,12 @@ model Spec {
|
|
|
63
63
|
// site adalah kelas bug SPEC-431/448/475. Tulis-sekali: reopen lalu selesai lagi tak
|
|
64
64
|
// memindahkannya, cermin `startedAt` = mulai pertama (ADR-0090).
|
|
65
65
|
doneAt DateTime?
|
|
66
|
+
// SPEC-804 · ADR-0120 · jejak penandaan selesai MANUAL: { at, by, reason? }. Satu kolom, bukan
|
|
67
|
+
// tiga skalar — ketiganya satu peristiwa, dan tiga kolom nullable bisa drift tanpa tipe yang
|
|
68
|
+
// memaksanya konsisten (kelas gagal-senyap ADR-0090/0093/0094/0105). null = item ini tak pernah
|
|
69
|
+
// ditandai manual. Ditimpa tiap penandaan berikutnya: ia menjelaskan keadaan yang BERLAKU;
|
|
70
|
+
// riwayat transisi stage tinggal di SessionResult (ADR-0047). Ikut FIELDS.spec sync.
|
|
71
|
+
manualDone Json?
|
|
66
72
|
// SPEC-447 · ADR-0093 · id spec lain yang harus SELESAI & commit-nya sudah ada di branch basis
|
|
67
73
|
// sebelum item ini boleh diluncurkan. Array JSON of string; null/[] = tak bergantung apa pun.
|
|
68
74
|
// Sengaja kolom, bukan tabel join: SQLite melarang scalar list, `Json` sudah dipakai `payload`,
|