hanoman 0.1.40 → 0.1.42
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 +159 -49
- 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-B7aHqLDK.js} +1539 -1539
- package/web/assets/index-TRbs6vrh.css +41 -0
- package/web/index.html +2 -2
- package/web/assets/index-BeFLNV7l.css +0 -41
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");
|
|
@@ -10155,12 +10173,24 @@ function broadcast(a, f) {
|
|
|
10155
10173
|
const msg = frame(f);
|
|
10156
10174
|
for (const c of a.clients) c.send(msg);
|
|
10157
10175
|
}
|
|
10176
|
+
function flushOutput(a) {
|
|
10177
|
+
if (a.flushTimer) {
|
|
10178
|
+
clearTimeout(a.flushTimer);
|
|
10179
|
+
a.flushTimer = void 0;
|
|
10180
|
+
}
|
|
10181
|
+
if (!a.pending) return;
|
|
10182
|
+
const d = a.pending;
|
|
10183
|
+
a.pending = "";
|
|
10184
|
+
a.scrollback = trimScrollback(a.scrollback + d);
|
|
10185
|
+
broadcast(a, { t: "data", d });
|
|
10186
|
+
}
|
|
10158
10187
|
function open(id) {
|
|
10159
10188
|
const pty = spawnPty("attach-session", "-d", "-t", name(id));
|
|
10160
|
-
const a = { pty, scrollback: "", clients: /* @__PURE__ */ new Set(), lastPhases: "" };
|
|
10189
|
+
const a = { pty, scrollback: "", clients: /* @__PURE__ */ new Set(), lastPhases: "", pending: "" };
|
|
10161
10190
|
pty.onData((d) => {
|
|
10162
|
-
a.
|
|
10163
|
-
|
|
10191
|
+
a.pending += d;
|
|
10192
|
+
if (a.pending.length >= COALESCE_MAX_BYTES) flushOutput(a);
|
|
10193
|
+
else if (!a.flushTimer) a.flushTimer = setTimeout(() => flushOutput(a), COALESCE_MS);
|
|
10164
10194
|
});
|
|
10165
10195
|
pty.onExit(() => {
|
|
10166
10196
|
if (attached.get(id) === a) drop(id);
|
|
@@ -10172,6 +10202,7 @@ function open(id) {
|
|
|
10172
10202
|
function drop(id) {
|
|
10173
10203
|
const a = attached.get(id);
|
|
10174
10204
|
if (!a) return;
|
|
10205
|
+
flushOutput(a);
|
|
10175
10206
|
attached.delete(id);
|
|
10176
10207
|
a.pty.kill();
|
|
10177
10208
|
for (const c of a.clients) c.close();
|
|
@@ -10180,6 +10211,7 @@ function drop(id) {
|
|
|
10180
10211
|
function end(id, code) {
|
|
10181
10212
|
const a = attached.get(id);
|
|
10182
10213
|
if (!a) return;
|
|
10214
|
+
flushOutput(a);
|
|
10183
10215
|
broadcast(a, { t: "exit", code });
|
|
10184
10216
|
drop(id);
|
|
10185
10217
|
}
|
|
@@ -10273,7 +10305,7 @@ function spawnPty(...args) {
|
|
|
10273
10305
|
);
|
|
10274
10306
|
}
|
|
10275
10307
|
}
|
|
10276
|
-
var socket, PREFIX, MAX_SCROLLBACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
10308
|
+
var socket, PREFIX, MAX_SCROLLBACK, SCROLLBACK_SLACK, POLL_MS, markerFilled, attached, claudeBin, shellBin, codexBin, agentBin, rootBypassEnv, frame, name, promptFilePath, goalGatePath, goalStatePath, agentsFilePath, NO_SERVER, TmuxError, sq, idFor, FMT, listSessions, liveDecisions, getSession, hooks, emitBirth, emitDeath, customAgentSource, customAgentsFor, sleep, paneText, dialogIO, DIALOG_CAPTURE_LINES, GOAL_ARMED_MARKERS, goalArmed, COALESCE_MS, COALESCE_MAX_BYTES, trimScrollback, phaseKey, paneComplete, sessionFinished, poll, detach;
|
|
10277
10309
|
var init_pty = __esm({
|
|
10278
10310
|
"src/services/pty.ts"() {
|
|
10279
10311
|
"use strict";
|
|
@@ -10287,6 +10319,7 @@ var init_pty = __esm({
|
|
|
10287
10319
|
socket = () => effectiveStr("HANOMAN_TMUX_SOCKET") ?? "hanoman";
|
|
10288
10320
|
PREFIX = "hanoman-";
|
|
10289
10321
|
MAX_SCROLLBACK = 256 * 1024;
|
|
10322
|
+
SCROLLBACK_SLACK = 64 * 1024;
|
|
10290
10323
|
POLL_MS = 500;
|
|
10291
10324
|
markerFilled = (f) => {
|
|
10292
10325
|
try {
|
|
@@ -10400,6 +10433,9 @@ var init_pty = __esm({
|
|
|
10400
10433
|
const text = paneText(id);
|
|
10401
10434
|
return GOAL_ARMED_MARKERS[agent].some((m) => text.includes(m));
|
|
10402
10435
|
};
|
|
10436
|
+
COALESCE_MS = 16;
|
|
10437
|
+
COALESCE_MAX_BYTES = 64 * 1024;
|
|
10438
|
+
trimScrollback = (s2) => s2.length > MAX_SCROLLBACK + SCROLLBACK_SLACK ? s2.slice(-MAX_SCROLLBACK) : s2;
|
|
10403
10439
|
phaseKey = (phases, complete) => JSON.stringify({ phases, complete });
|
|
10404
10440
|
paneComplete = (p) => !!p.flow && !!p.phaseFile && sessionComplete(readPhases(p.phaseFile, p.flow), p.cwd, p.specId);
|
|
10405
10441
|
sessionFinished = (id) => {
|
|
@@ -10531,6 +10567,7 @@ __export(sync_exports, {
|
|
|
10531
10567
|
__DATE_FIELDS: () => __DATE_FIELDS,
|
|
10532
10568
|
__FIELDS: () => __FIELDS,
|
|
10533
10569
|
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
10570
|
+
__JSON_FIELDS: () => __JSON_FIELDS,
|
|
10534
10571
|
applyPush: () => applyPush,
|
|
10535
10572
|
backfillFeed: () => backfillFeed,
|
|
10536
10573
|
consumeTombstoneOnRecreate: () => consumeTombstoneOnRecreate,
|
|
@@ -10774,7 +10811,7 @@ async function upsertLocal(entity, id, version2, data) {
|
|
|
10774
10811
|
function setAcceptedHook(hook) {
|
|
10775
10812
|
onAccepted = hook;
|
|
10776
10813
|
}
|
|
10777
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10814
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, PARENTS, __FIELDS, __DATE_FIELDS, NUMBER_FIELDS, BOOLEAN_FIELDS, JSON_FIELDS, __JSON_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
10778
10815
|
var init_sync = __esm({
|
|
10779
10816
|
"src/services/sync.ts"() {
|
|
10780
10817
|
"use strict";
|
|
@@ -10806,7 +10843,10 @@ var init_sync = __esm({
|
|
|
10806
10843
|
// TETAP berhasil, jadi kolom yang terlewat di sini mendarat sebagai null palsu di tiap client
|
|
10807
10844
|
// tanpa satu pun error (kelas gagal-senyap ADR-0090/0093/0094/0105). BUKAN DATE_FIELDS —
|
|
10808
10845
|
// `at` hidup di dalam JSON-nya, kolomnya sendiri bukan DateTime.
|
|
10809
|
-
|
|
10846
|
+
// SPEC-804 · ADR-0120 · manualDone ikut menyeberang: "item ini ditandai selesai manusia" adalah
|
|
10847
|
+
// bagian keadaan yang harus dilihat sama oleh semua mesin — di antaranya gerbang auto-merge.
|
|
10848
|
+
// BUKAN DATE_FIELDS, alasan yang sama dengan sourceHistory.
|
|
10849
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "sourceHistory", "manualDone", "createdAt", "startedAt", "doneAt", "updatedAt"],
|
|
10810
10850
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
10811
10851
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
10812
10852
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
@@ -10874,12 +10914,14 @@ var init_sync = __esm({
|
|
|
10874
10914
|
"spec:payload",
|
|
10875
10915
|
"spec:dependsOn",
|
|
10876
10916
|
"spec:sourceHistory",
|
|
10917
|
+
"spec:manualDone",
|
|
10877
10918
|
"vps:health",
|
|
10878
10919
|
"vps:audit",
|
|
10879
10920
|
"customAgent:tools",
|
|
10880
10921
|
"customAgent:mentions",
|
|
10881
10922
|
"githubIssue:labels"
|
|
10882
10923
|
]);
|
|
10924
|
+
__JSON_FIELDS = JSON_FIELDS;
|
|
10883
10925
|
__FIELDS_FOR_TEST = FIELDS;
|
|
10884
10926
|
}
|
|
10885
10927
|
});
|
|
@@ -25773,6 +25815,65 @@ function appendSourceHistory(current, entry) {
|
|
|
25773
25815
|
|
|
25774
25816
|
// src/routes/specs.ts
|
|
25775
25817
|
init_notifications();
|
|
25818
|
+
|
|
25819
|
+
// src/services/spec-complete.ts
|
|
25820
|
+
init_db();
|
|
25821
|
+
init_notifications();
|
|
25822
|
+
|
|
25823
|
+
// src/services/session-result.ts
|
|
25824
|
+
init_db();
|
|
25825
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
25826
|
+
var WHITELIST = [
|
|
25827
|
+
"projectId",
|
|
25828
|
+
"specId",
|
|
25829
|
+
"oldStage",
|
|
25830
|
+
"newStage",
|
|
25831
|
+
"commitSha",
|
|
25832
|
+
"branch",
|
|
25833
|
+
"prUrl",
|
|
25834
|
+
"status",
|
|
25835
|
+
"deviceId",
|
|
25836
|
+
"author"
|
|
25837
|
+
];
|
|
25838
|
+
async function recordSessionResult(input) {
|
|
25839
|
+
const id = randomUUID3();
|
|
25840
|
+
const data = { id };
|
|
25841
|
+
for (const f of WHITELIST) if (input[f] !== void 0 && input[f] !== null) data[f] = input[f];
|
|
25842
|
+
if (data.status === void 0) data.status = "done";
|
|
25843
|
+
if (data.projectId === void 0) throw new Error("session result butuh projectId");
|
|
25844
|
+
await prisma.sessionResult.create({ data });
|
|
25845
|
+
await notifySynced("sessionResult", id);
|
|
25846
|
+
return { id };
|
|
25847
|
+
}
|
|
25848
|
+
|
|
25849
|
+
// src/services/spec-complete.ts
|
|
25850
|
+
async function completeSpecManually(spec, input) {
|
|
25851
|
+
const at = input.at ?? /* @__PURE__ */ new Date();
|
|
25852
|
+
const manualDone = {
|
|
25853
|
+
at: at.toISOString(),
|
|
25854
|
+
by: input.by,
|
|
25855
|
+
...input.reason ? { reason: input.reason } : {}
|
|
25856
|
+
};
|
|
25857
|
+
const { count } = await prisma.spec.updateMany({
|
|
25858
|
+
where: { id: spec.id, stage: { not: "done" } },
|
|
25859
|
+
data: { stage: "done", manualDone }
|
|
25860
|
+
});
|
|
25861
|
+
if (count === 0) return { ok: false };
|
|
25862
|
+
await recordCompletion(spec.id, spec.title, spec.projectId);
|
|
25863
|
+
await recordSessionResult({
|
|
25864
|
+
projectId: spec.projectId,
|
|
25865
|
+
specId: spec.id,
|
|
25866
|
+
oldStage: spec.stage,
|
|
25867
|
+
newStage: "done",
|
|
25868
|
+
status: "done",
|
|
25869
|
+
author: input.by
|
|
25870
|
+
}).catch(() => {
|
|
25871
|
+
});
|
|
25872
|
+
await notifySynced("spec", spec.id);
|
|
25873
|
+
return { ok: true, spec: await prisma.spec.findUnique({ where: { id: spec.id } }) };
|
|
25874
|
+
}
|
|
25875
|
+
|
|
25876
|
+
// src/routes/specs.ts
|
|
25776
25877
|
init_stage_machine();
|
|
25777
25878
|
|
|
25778
25879
|
// src/services/stage-artifacts.ts
|
|
@@ -28693,6 +28794,23 @@ ${item.context}` : item.context;
|
|
|
28693
28794
|
await notifySynced("spec", id);
|
|
28694
28795
|
return updated;
|
|
28695
28796
|
});
|
|
28797
|
+
app2.post("/specs/:id/done", async (req, reply) => {
|
|
28798
|
+
const { id } = req.params;
|
|
28799
|
+
const parsed = zMarkSpecDone.safeParse(req.body ?? {});
|
|
28800
|
+
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
28801
|
+
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
28802
|
+
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
28803
|
+
if (spec.stage === "done") return reply.code(409).send({ error: "backlog item sudah selesai" });
|
|
28804
|
+
const live = listSessions().find((s2) => s2.specId === id && !s2.exited);
|
|
28805
|
+
if (live && parsed.data.confirm !== true)
|
|
28806
|
+
return reply.code(409).send({ error: "confirm-required", session: { id: live.id, agent: live.agent } });
|
|
28807
|
+
const res = await completeSpecManually(spec, {
|
|
28808
|
+
by: req.user?.email ?? "system",
|
|
28809
|
+
reason: parsed.data.reason || void 0
|
|
28810
|
+
});
|
|
28811
|
+
if (!res.ok) return reply.code(409).send({ error: "backlog item sudah selesai" });
|
|
28812
|
+
return res.spec;
|
|
28813
|
+
});
|
|
28696
28814
|
app2.get("/specs/:id/docs", async (req) => ({ files: await listSpecDocs(req.params.id) }));
|
|
28697
28815
|
app2.get("/specs/:id/docs/*", async (req, reply) => {
|
|
28698
28816
|
const { id } = req.params;
|
|
@@ -30520,32 +30638,6 @@ function startWorktreeReaper(deps = prodReaperDeps) {
|
|
|
30520
30638
|
timer.unref();
|
|
30521
30639
|
}
|
|
30522
30640
|
|
|
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
30641
|
// src/routes/terminal.ts
|
|
30550
30642
|
init_notifications();
|
|
30551
30643
|
init_stage_machine();
|
|
@@ -40487,9 +40579,9 @@ async function help_default(app2) {
|
|
|
40487
40579
|
return reply.code(201).send({ number: ticket.number, key, statusPath });
|
|
40488
40580
|
});
|
|
40489
40581
|
app2.get("/help/:slug/tickets/:key", async (req, reply) => {
|
|
40490
|
-
const {
|
|
40582
|
+
const { key } = req.params;
|
|
40491
40583
|
const t = await prisma.ticket.findFirst({
|
|
40492
|
-
where: {
|
|
40584
|
+
where: { OR: [{ accessKeyHash: hashAccessKey(key) }, { shareToken: key }] }
|
|
40493
40585
|
});
|
|
40494
40586
|
if (!t) return reply.code(404).send({ error: "not found" });
|
|
40495
40587
|
let stage = null;
|
|
@@ -40594,7 +40686,7 @@ var view6 = (t) => ({
|
|
|
40594
40686
|
attachmentCount: t._count?.attachments ?? 0,
|
|
40595
40687
|
createdAt: t.createdAt.toISOString()
|
|
40596
40688
|
});
|
|
40597
|
-
async function tickets_default(app2) {
|
|
40689
|
+
async function tickets_default(app2, opts = {}) {
|
|
40598
40690
|
app2.get("/tickets", async (req) => {
|
|
40599
40691
|
const { project, status, q, page, limit } = req.query;
|
|
40600
40692
|
const where = {};
|
|
@@ -40625,7 +40717,7 @@ async function tickets_default(app2) {
|
|
|
40625
40717
|
shareToken = generateShareToken();
|
|
40626
40718
|
await prisma.ticket.update({ where: { id }, data: { shareToken } });
|
|
40627
40719
|
}
|
|
40628
|
-
const base2 = `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40720
|
+
const base2 = opts.publicBase ?? `${req.protocol}://${req.headers.host ?? "localhost"}`;
|
|
40629
40721
|
const publicStatusUrl = `${base2}/help/${encodeURIComponent(t.projectId)}/status/${shareToken}`;
|
|
40630
40722
|
return {
|
|
40631
40723
|
...view6(t),
|
|
@@ -45143,22 +45235,31 @@ init_runtime();
|
|
|
45143
45235
|
|
|
45144
45236
|
// src/services/ingress-policy.ts
|
|
45145
45237
|
function origins(value, key) {
|
|
45146
|
-
const out3 =
|
|
45238
|
+
const out3 = [];
|
|
45147
45239
|
for (const raw2 of value?.split(",") ?? []) {
|
|
45148
45240
|
const input = raw2.trim();
|
|
45149
45241
|
if (!input) continue;
|
|
45150
45242
|
const parsed = new URL(input);
|
|
45151
45243
|
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password || parsed.pathname !== "/")
|
|
45152
45244
|
throw new Error(`${key} harus berisi exact http(s) origin`);
|
|
45153
|
-
out3.
|
|
45245
|
+
out3.push({ host: parsed.host.toLowerCase(), base: `${parsed.protocol}//${parsed.host}` });
|
|
45154
45246
|
}
|
|
45155
45247
|
return out3;
|
|
45156
45248
|
}
|
|
45157
45249
|
function loadIngressPolicy(env) {
|
|
45158
|
-
const
|
|
45159
|
-
const
|
|
45250
|
+
const pub = origins(env.HANOMAN_PUBLIC_ORIGINS, "HANOMAN_PUBLIC_ORIGINS");
|
|
45251
|
+
const control = origins(env.HANOMAN_CONTROL_ORIGINS, "HANOMAN_CONTROL_ORIGINS");
|
|
45252
|
+
const publicHosts = new Set(pub.map((o) => o.host));
|
|
45253
|
+
const controlHosts = new Set(control.map((o) => o.host));
|
|
45160
45254
|
for (const host2 of publicHosts) if (controlHosts.has(host2)) throw new Error("public dan control origin harus berbeda");
|
|
45161
|
-
return {
|
|
45255
|
+
return {
|
|
45256
|
+
publicHosts,
|
|
45257
|
+
controlHosts,
|
|
45258
|
+
// SPEC-805 · origin publik pertama, lengkap dengan scheme-nya: satu-satunya basis sah untuk
|
|
45259
|
+
// link status yang dibagikan operator, yang selalu lahir di request ber-host control.
|
|
45260
|
+
publicBase: pub[0]?.base ?? null,
|
|
45261
|
+
enforce: publicHosts.size > 0 || controlHosts.size > 0
|
|
45262
|
+
};
|
|
45162
45263
|
}
|
|
45163
45264
|
function publicPath(method, path2) {
|
|
45164
45265
|
if (method === "GET" && path2 === "/api/health") return true;
|
|
@@ -45170,7 +45271,8 @@ function classifyIngress(req, policy) {
|
|
|
45170
45271
|
const host2 = req.host.trim().toLowerCase();
|
|
45171
45272
|
const path2 = req.url.split("?")[0] ?? req.url;
|
|
45172
45273
|
if (policy.publicHosts.has(host2)) return publicPath(req.method, path2) ? "public" : "denied";
|
|
45173
|
-
if (policy.controlHosts.has(host2))
|
|
45274
|
+
if (policy.controlHosts.has(host2))
|
|
45275
|
+
return policy.publicBase && path2.startsWith("/api/help") ? "denied" : "control";
|
|
45174
45276
|
return "denied";
|
|
45175
45277
|
}
|
|
45176
45278
|
function trustProxyFromEnv(env) {
|
|
@@ -45203,6 +45305,8 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45203
45305
|
app2.addHook("onRequest", async (req, reply) => {
|
|
45204
45306
|
const role = classifyIngress({ host: req.headers.host ?? "", method: req.method, url: req.url }, ingress);
|
|
45205
45307
|
if (role === "denied") return reply.code(404).send({ error: "not found" });
|
|
45308
|
+
if (role === "control" && ingress.publicBase && req.url.startsWith("/help/"))
|
|
45309
|
+
return reply.redirect(`${ingress.publicBase}${req.url}`, 302);
|
|
45206
45310
|
});
|
|
45207
45311
|
app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
45208
45312
|
if (!body) return done(null, void 0);
|
|
@@ -45213,7 +45317,12 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45213
45317
|
done(err, void 0);
|
|
45214
45318
|
}
|
|
45215
45319
|
});
|
|
45216
|
-
app2.register(websocket, {
|
|
45320
|
+
app2.register(websocket, {
|
|
45321
|
+
options: {
|
|
45322
|
+
maxPayload: MAX_WS_MESSAGE_BYTES,
|
|
45323
|
+
perMessageDeflate: { zlibDeflateOptions: { level: 6, memLevel: 7 }, concurrencyLimit: 10 }
|
|
45324
|
+
}
|
|
45325
|
+
});
|
|
45217
45326
|
app2.addHook("onClose", async () => {
|
|
45218
45327
|
await stopTelegramRuntime();
|
|
45219
45328
|
detachAll();
|
|
@@ -45282,7 +45391,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
|
|
|
45282
45391
|
await api.register(session_history_default);
|
|
45283
45392
|
await api.register(config_default);
|
|
45284
45393
|
await api.register(help_default);
|
|
45285
|
-
await api.register(tickets_default);
|
|
45394
|
+
await api.register(tickets_default, { publicBase: ingress.publicBase });
|
|
45286
45395
|
await api.register(scheduler_default);
|
|
45287
45396
|
await api.register(codex);
|
|
45288
45397
|
await api.register(methods);
|
|
@@ -45975,6 +46084,7 @@ async function sweepAutoMerge(deps = prodAutoMergeDeps, now = /* @__PURE__ */ ne
|
|
|
45975
46084
|
async function settleOne(c, deps, now) {
|
|
45976
46085
|
const spec = await prisma.spec.findUnique({ where: { id: c.specId } });
|
|
45977
46086
|
if (!spec || spec.stage !== "done") return false;
|
|
46087
|
+
if (spec.manualDone) return false;
|
|
45978
46088
|
const project = await prisma.project.findUnique({ where: { id: spec.projectId } });
|
|
45979
46089
|
if (!project) return false;
|
|
45980
46090
|
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`,
|