hanoman 0.1.7 → 0.1.8
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 +24 -5
- package/dist/server.js +245 -22
- package/package.json +1 -1
- package/prisma/migrations/20260731210000_spec_depends_on/migration.sql +7 -0
- package/prisma/schema.prisma +6 -0
- package/web/assets/{index-BWyQwoAk.js → index-D6AOUdRy.js} +1533 -1533
- package/web/index.html +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4357,7 +4357,7 @@ var init_enums = __esm({
|
|
|
4357
4357
|
});
|
|
4358
4358
|
|
|
4359
4359
|
// ../shared/src/entities.ts
|
|
4360
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpec, NOTIFY_SOUNDS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4360
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, zCodex, CODEX_DEFAULTS, zSourceCommon, zScheduler, SCHEDULER_DEFAULTS, zGoal, GOAL_DEFAULTS, zConflict, CONFLICT_DEFAULTS, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4361
4361
|
var init_entities = __esm({
|
|
4362
4362
|
"../shared/src/entities.ts"() {
|
|
4363
4363
|
"use strict";
|
|
@@ -4400,6 +4400,10 @@ var init_entities = __esm({
|
|
|
4400
4400
|
constraints: external_exports.string(),
|
|
4401
4401
|
priority: zPriority
|
|
4402
4402
|
});
|
|
4403
|
+
zSpecBlocker = external_exports.object({
|
|
4404
|
+
id: external_exports.string(),
|
|
4405
|
+
reason: external_exports.enum(["missing", "unfinished", "unmerged"])
|
|
4406
|
+
});
|
|
4403
4407
|
zSpec = external_exports.object({
|
|
4404
4408
|
id: external_exports.string(),
|
|
4405
4409
|
projectId: external_exports.string(),
|
|
@@ -4418,7 +4422,13 @@ var init_entities = __esm({
|
|
|
4418
4422
|
// SPEC-408 · ADR-0090 · stempel waktu backlog (ISO string di wire — kolom DateTime di DB).
|
|
4419
4423
|
// `startedAt` null = belum pernah dikerjakan; ia tak pernah ditulis ulang saat sesi dilanjutkan.
|
|
4420
4424
|
createdAt: external_exports.string(),
|
|
4421
|
-
startedAt: external_exports.string().nullable()
|
|
4425
|
+
startedAt: external_exports.string().nullable(),
|
|
4426
|
+
// SPEC-447 · ADR-0093 · id backlog yang harus selesai & ter-merge lebih dulu. Server selalu
|
|
4427
|
+
// menormalkannya ke array (kolom DB-nya `Json?`); `.default([])` menjaga respons lama.
|
|
4428
|
+
dependsOn: external_exports.array(external_exports.string()).default([]),
|
|
4429
|
+
// Turunan (bukan kolom): dihitung `liveSpecs` dari stage dependency + git. Klien tak pernah
|
|
4430
|
+
// mengirimkannya — `.default([])` supaya bentuk lama tetap parse.
|
|
4431
|
+
blockedBy: external_exports.array(zSpecBlocker).default([])
|
|
4422
4432
|
});
|
|
4423
4433
|
NOTIFY_SOUNDS = [
|
|
4424
4434
|
"off",
|
|
@@ -4758,7 +4768,9 @@ var init_dto = __esm({
|
|
|
4758
4768
|
title: external_exports.string().min(1),
|
|
4759
4769
|
priority: zPriority,
|
|
4760
4770
|
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]),
|
|
4761
|
-
branchFrom: external_exports.string().min(1).optional()
|
|
4771
|
+
branchFrom: external_exports.string().min(1).optional(),
|
|
4772
|
+
// SPEC-447 · ADR-0093 · divalidasi server (id ada / satu project / bukan diri sendiri / non-siklus).
|
|
4773
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4762
4774
|
}).superRefine((o, ctx) => {
|
|
4763
4775
|
const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
|
|
4764
4776
|
const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
|
|
@@ -4772,8 +4784,12 @@ var init_dto = __esm({
|
|
|
4772
4784
|
// SPEC-186 · edit konten selagi item belum dimulai. Ditolak server bila sudah mulai.
|
|
4773
4785
|
title: external_exports.string().min(1).optional(),
|
|
4774
4786
|
priority: zPriority.optional(),
|
|
4775
|
-
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
|
|
4787
|
+
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional(),
|
|
4776
4788
|
// SPEC-407 · +goal
|
|
4789
|
+
// SPEC-447 · ADR-0093 · SENGAJA di luar gerbang `editingContent` (SPEC-186): gerbang itu
|
|
4790
|
+
// melindungi konten yang sudah jadi dasar kerja sesi berjalan, sedangkan dependsOn hanya
|
|
4791
|
+
// menggerbangi peluncuran BERIKUTNYA. `[]` = kosongkan.
|
|
4792
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4777
4793
|
});
|
|
4778
4794
|
zIntegrate = external_exports.object({
|
|
4779
4795
|
op: external_exports.enum(["merge", "rebase"]),
|
|
@@ -4969,7 +4985,10 @@ var init_dto = __esm({
|
|
|
4969
4985
|
goal: external_exports.boolean().optional(),
|
|
4970
4986
|
goalCondition: external_exports.string().max(4e3).optional(),
|
|
4971
4987
|
agent: zAgent.optional(),
|
|
4972
|
-
verifyScope: zVerifyScope.optional()
|
|
4988
|
+
verifyScope: zVerifyScope.optional(),
|
|
4989
|
+
// SPEC-447 · ADR-0093 — lewati gerbang dependency. Hanya jalur manusia; UI hanya
|
|
4990
|
+
// mengirimkannya sesudah operator melihat daftar pemblokirnya.
|
|
4991
|
+
force: external_exports.boolean().optional()
|
|
4973
4992
|
})
|
|
4974
4993
|
]);
|
|
4975
4994
|
zDocFileContent = external_exports.object({ content: external_exports.string() });
|
package/dist/server.js
CHANGED
|
@@ -4182,7 +4182,7 @@ function cmpVersion(a, b) {
|
|
|
4182
4182
|
}
|
|
4183
4183
|
return 0;
|
|
4184
4184
|
}
|
|
4185
|
-
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpec, NOTIFY_SOUNDS, 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, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4185
|
+
var zProject, zBriefPayload, zQaPayload, zGoalPayload, zSpecBlocker, zSpec, NOTIFY_SOUNDS, 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, zLeadEngine, zLead, LEAD_DEFAULTS, zSetting, zNotification, zDocFile, zDeviceTokenView;
|
|
4186
4186
|
var init_entities = __esm({
|
|
4187
4187
|
"../shared/src/entities.ts"() {
|
|
4188
4188
|
"use strict";
|
|
@@ -4225,6 +4225,10 @@ var init_entities = __esm({
|
|
|
4225
4225
|
constraints: external_exports.string(),
|
|
4226
4226
|
priority: zPriority
|
|
4227
4227
|
});
|
|
4228
|
+
zSpecBlocker = external_exports.object({
|
|
4229
|
+
id: external_exports.string(),
|
|
4230
|
+
reason: external_exports.enum(["missing", "unfinished", "unmerged"])
|
|
4231
|
+
});
|
|
4228
4232
|
zSpec = external_exports.object({
|
|
4229
4233
|
id: external_exports.string(),
|
|
4230
4234
|
projectId: external_exports.string(),
|
|
@@ -4243,7 +4247,13 @@ var init_entities = __esm({
|
|
|
4243
4247
|
// SPEC-408 · ADR-0090 · stempel waktu backlog (ISO string di wire — kolom DateTime di DB).
|
|
4244
4248
|
// `startedAt` null = belum pernah dikerjakan; ia tak pernah ditulis ulang saat sesi dilanjutkan.
|
|
4245
4249
|
createdAt: external_exports.string(),
|
|
4246
|
-
startedAt: external_exports.string().nullable()
|
|
4250
|
+
startedAt: external_exports.string().nullable(),
|
|
4251
|
+
// SPEC-447 · ADR-0093 · id backlog yang harus selesai & ter-merge lebih dulu. Server selalu
|
|
4252
|
+
// menormalkannya ke array (kolom DB-nya `Json?`); `.default([])` menjaga respons lama.
|
|
4253
|
+
dependsOn: external_exports.array(external_exports.string()).default([]),
|
|
4254
|
+
// Turunan (bukan kolom): dihitung `liveSpecs` dari stage dependency + git. Klien tak pernah
|
|
4255
|
+
// mengirimkannya — `.default([])` supaya bentuk lama tetap parse.
|
|
4256
|
+
blockedBy: external_exports.array(zSpecBlocker).default([])
|
|
4247
4257
|
});
|
|
4248
4258
|
NOTIFY_SOUNDS = [
|
|
4249
4259
|
"off",
|
|
@@ -4650,7 +4660,9 @@ var init_dto = __esm({
|
|
|
4650
4660
|
title: external_exports.string().min(1),
|
|
4651
4661
|
priority: zPriority,
|
|
4652
4662
|
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]),
|
|
4653
|
-
branchFrom: external_exports.string().min(1).optional()
|
|
4663
|
+
branchFrom: external_exports.string().min(1).optional(),
|
|
4664
|
+
// SPEC-447 · ADR-0093 · divalidasi server (id ada / satu project / bukan diri sendiri / non-siklus).
|
|
4665
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4654
4666
|
}).superRefine((o, ctx) => {
|
|
4655
4667
|
const shape = "severity" in o.payload ? "qa" : "goal" in o.payload ? "goal" : "brief";
|
|
4656
4668
|
const want = o.source === "qa" ? "qa" : o.source === "goal" ? "goal" : "brief";
|
|
@@ -4664,8 +4676,12 @@ var init_dto = __esm({
|
|
|
4664
4676
|
// SPEC-186 · edit konten selagi item belum dimulai. Ditolak server bila sudah mulai.
|
|
4665
4677
|
title: external_exports.string().min(1).optional(),
|
|
4666
4678
|
priority: zPriority.optional(),
|
|
4667
|
-
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional()
|
|
4679
|
+
payload: external_exports.union([zBriefPayload, zQaPayload, zGoalPayload]).optional(),
|
|
4668
4680
|
// SPEC-407 · +goal
|
|
4681
|
+
// SPEC-447 · ADR-0093 · SENGAJA di luar gerbang `editingContent` (SPEC-186): gerbang itu
|
|
4682
|
+
// melindungi konten yang sudah jadi dasar kerja sesi berjalan, sedangkan dependsOn hanya
|
|
4683
|
+
// menggerbangi peluncuran BERIKUTNYA. `[]` = kosongkan.
|
|
4684
|
+
dependsOn: external_exports.array(external_exports.string()).optional()
|
|
4669
4685
|
});
|
|
4670
4686
|
zIntegrate = external_exports.object({
|
|
4671
4687
|
op: external_exports.enum(["merge", "rebase"]),
|
|
@@ -4861,7 +4877,10 @@ var init_dto = __esm({
|
|
|
4861
4877
|
goal: external_exports.boolean().optional(),
|
|
4862
4878
|
goalCondition: external_exports.string().max(4e3).optional(),
|
|
4863
4879
|
agent: zAgent.optional(),
|
|
4864
|
-
verifyScope: zVerifyScope.optional()
|
|
4880
|
+
verifyScope: zVerifyScope.optional(),
|
|
4881
|
+
// SPEC-447 · ADR-0093 — lewati gerbang dependency. Hanya jalur manusia; UI hanya
|
|
4882
|
+
// mengirimkannya sesudah operator melihat daftar pemblokirnya.
|
|
4883
|
+
force: external_exports.boolean().optional()
|
|
4865
4884
|
})
|
|
4866
4885
|
]);
|
|
4867
4886
|
zDocFileContent = external_exports.object({ content: external_exports.string() });
|
|
@@ -5989,6 +6008,17 @@ var init_git = __esm({
|
|
|
5989
6008
|
{ cwd: repo, encoding: "utf8" }
|
|
5990
6009
|
);
|
|
5991
6010
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
6011
|
+
},
|
|
6012
|
+
// SPEC-447 · `git merge-base --is-ancestor A B` = exit 0 (ya) / 1 (tidak) / lainnya (error).
|
|
6013
|
+
// `--end-of-options` menjaga ADR-0032: sha & ref datang dari DB/kolom, jangan sampai terbaca
|
|
6014
|
+
// sebagai flag. Ref yang tak resolve membuat git exit 128 → dibaca sebagai "belum" (fail-closed).
|
|
6015
|
+
isAncestor: (repo, sha, ref) => {
|
|
6016
|
+
const r = spawnSync(
|
|
6017
|
+
"git",
|
|
6018
|
+
["merge-base", "--is-ancestor", "--end-of-options", sha, ref],
|
|
6019
|
+
{ cwd: repo, encoding: "utf8" }
|
|
6020
|
+
);
|
|
6021
|
+
return r.status === 0;
|
|
5992
6022
|
}
|
|
5993
6023
|
};
|
|
5994
6024
|
}
|
|
@@ -7000,6 +7030,7 @@ var init_rename_project = __esm({
|
|
|
7000
7030
|
var sync_exports = {};
|
|
7001
7031
|
__export(sync_exports, {
|
|
7002
7032
|
SYNCED: () => SYNCED,
|
|
7033
|
+
__FIELDS_FOR_TEST: () => __FIELDS_FOR_TEST,
|
|
7003
7034
|
applyPush: () => applyPush,
|
|
7004
7035
|
backfillFeed: () => backfillFeed,
|
|
7005
7036
|
isEntity: () => isEntity,
|
|
@@ -7137,7 +7168,7 @@ async function upsertLocal(entity, id, version, data) {
|
|
|
7137
7168
|
function setAcceptedHook(hook) {
|
|
7138
7169
|
onAccepted = hook;
|
|
7139
7170
|
}
|
|
7140
|
-
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted;
|
|
7171
|
+
var SYNCED, DELEGATE, FIELDS, DATE_FIELDS, onAccepted, __FIELDS_FOR_TEST;
|
|
7141
7172
|
var init_sync = __esm({
|
|
7142
7173
|
"src/services/sync.ts"() {
|
|
7143
7174
|
"use strict";
|
|
@@ -7156,7 +7187,9 @@ var init_sync = __esm({
|
|
|
7156
7187
|
project: ["name", "desc", "kind", "stack", "gitRemote", "updatedAt"],
|
|
7157
7188
|
// SPEC-408 · ADR-0090 · createdAt/startedAt ikut menyeberang — sejajar baseSha/headSha. Tanpa
|
|
7158
7189
|
// ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
|
|
7159
|
-
|
|
7190
|
+
// SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
|
|
7191
|
+
// meluncurkan pekerjaan yang di hub terblokir. Bukan DATE_FIELDS — nilainya array string.
|
|
7192
|
+
spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "createdAt", "startedAt", "updatedAt"],
|
|
7160
7193
|
vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
|
|
7161
7194
|
sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
|
|
7162
7195
|
// SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
|
|
@@ -7173,6 +7206,7 @@ var init_sync = __esm({
|
|
|
7173
7206
|
ticket: ["createdAt", "updatedAt"],
|
|
7174
7207
|
ticketAttachment: ["createdAt", "updatedAt"]
|
|
7175
7208
|
};
|
|
7209
|
+
__FIELDS_FOR_TEST = FIELDS;
|
|
7176
7210
|
}
|
|
7177
7211
|
});
|
|
7178
7212
|
|
|
@@ -15360,6 +15394,128 @@ async function readEscalation(specId, sessions = listSessions()) {
|
|
|
15360
15394
|
};
|
|
15361
15395
|
}
|
|
15362
15396
|
|
|
15397
|
+
// src/services/spec-deps.ts
|
|
15398
|
+
init_db();
|
|
15399
|
+
init_src2();
|
|
15400
|
+
var REASON_LABEL = {
|
|
15401
|
+
missing: "tak ditemukan",
|
|
15402
|
+
unfinished: "belum selesai",
|
|
15403
|
+
unmerged: "belum ter-merge"
|
|
15404
|
+
};
|
|
15405
|
+
function dependsOnOf(spec) {
|
|
15406
|
+
const v = spec.dependsOn;
|
|
15407
|
+
if (!Array.isArray(v)) return [];
|
|
15408
|
+
const out3 = [];
|
|
15409
|
+
for (const x of v) if (typeof x === "string" && x !== "" && !out3.includes(x)) out3.push(x);
|
|
15410
|
+
return out3;
|
|
15411
|
+
}
|
|
15412
|
+
function blockersFor(spec, deps, isMerged) {
|
|
15413
|
+
const ids = dependsOnOf(spec);
|
|
15414
|
+
if (ids.length === 0) return [];
|
|
15415
|
+
const base = spec.branchFrom ?? "HEAD";
|
|
15416
|
+
const out3 = [];
|
|
15417
|
+
for (const id of ids) {
|
|
15418
|
+
const d = deps.get(id);
|
|
15419
|
+
if (!d) {
|
|
15420
|
+
out3.push({ id, reason: "missing" });
|
|
15421
|
+
continue;
|
|
15422
|
+
}
|
|
15423
|
+
if (d.stage !== "done") {
|
|
15424
|
+
out3.push({ id, reason: "unfinished" });
|
|
15425
|
+
continue;
|
|
15426
|
+
}
|
|
15427
|
+
if (d.headSha && !isMerged(d.headSha, base)) out3.push({ id, reason: "unmerged" });
|
|
15428
|
+
}
|
|
15429
|
+
return out3;
|
|
15430
|
+
}
|
|
15431
|
+
function blockedNote(bl) {
|
|
15432
|
+
return `menunggu ${bl.map((b) => `${b.id} (${REASON_LABEL[b.reason]})`).join(", ")}`;
|
|
15433
|
+
}
|
|
15434
|
+
function reaches(edges, from, target2) {
|
|
15435
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15436
|
+
const stack = [...from];
|
|
15437
|
+
while (stack.length) {
|
|
15438
|
+
const cur = stack.pop();
|
|
15439
|
+
if (cur === target2) return true;
|
|
15440
|
+
if (seen.has(cur)) continue;
|
|
15441
|
+
seen.add(cur);
|
|
15442
|
+
for (const n of edges.get(cur) ?? []) stack.push(n);
|
|
15443
|
+
}
|
|
15444
|
+
return false;
|
|
15445
|
+
}
|
|
15446
|
+
var TTL_MS = 15e3;
|
|
15447
|
+
var mergeCache = /* @__PURE__ */ new Map();
|
|
15448
|
+
function mergedInto(repoDir, sha, baseRef) {
|
|
15449
|
+
const key = `${repoDir} ${sha} ${baseRef}`;
|
|
15450
|
+
const hit = mergeCache.get(key);
|
|
15451
|
+
const now = Date.now();
|
|
15452
|
+
if (hit && now - hit.at < TTL_MS) return hit.v;
|
|
15453
|
+
let v = false;
|
|
15454
|
+
try {
|
|
15455
|
+
v = realGit.isAncestor(repoDir, sha, baseRef);
|
|
15456
|
+
} catch {
|
|
15457
|
+
v = false;
|
|
15458
|
+
}
|
|
15459
|
+
mergeCache.set(key, { at: now, v });
|
|
15460
|
+
return v;
|
|
15461
|
+
}
|
|
15462
|
+
var merger = (repoDir) => (sha, base) => repoDir ? mergedInto(repoDir, sha, base) : false;
|
|
15463
|
+
var depRows = (ids) => prisma.spec.findMany({
|
|
15464
|
+
where: { id: { in: ids } },
|
|
15465
|
+
select: { id: true, stage: true, headSha: true }
|
|
15466
|
+
});
|
|
15467
|
+
async function blockersForSpec(spec, repoDir) {
|
|
15468
|
+
const ids = dependsOnOf(spec);
|
|
15469
|
+
if (ids.length === 0) return [];
|
|
15470
|
+
const rows = await depRows(ids);
|
|
15471
|
+
return blockersFor(spec, new Map(rows.map((r) => [r.id, r])), merger(repoDir));
|
|
15472
|
+
}
|
|
15473
|
+
async function decorateBlocked(specs) {
|
|
15474
|
+
const ids = [...new Set(specs.flatMap(dependsOnOf))];
|
|
15475
|
+
if (ids.length === 0) return specs.map((s) => ({ ...s, dependsOn: [], blockedBy: [] }));
|
|
15476
|
+
const rows = await depRows(ids);
|
|
15477
|
+
const deps = new Map(rows.map((r) => [r.id, r]));
|
|
15478
|
+
const repos = /* @__PURE__ */ new Map();
|
|
15479
|
+
const out3 = [];
|
|
15480
|
+
for (const s of specs) {
|
|
15481
|
+
const own = dependsOnOf(s);
|
|
15482
|
+
if (own.length === 0) {
|
|
15483
|
+
out3.push({ ...s, dependsOn: [], blockedBy: [] });
|
|
15484
|
+
continue;
|
|
15485
|
+
}
|
|
15486
|
+
if (!repos.has(s.projectId)) repos.set(s.projectId, await resolveRepoDir(s.projectId));
|
|
15487
|
+
out3.push({ ...s, dependsOn: own, blockedBy: blockersFor(s, deps, merger(repos.get(s.projectId))) });
|
|
15488
|
+
}
|
|
15489
|
+
return out3;
|
|
15490
|
+
}
|
|
15491
|
+
async function validateDependsOn(specId, projectId, raw) {
|
|
15492
|
+
const ids = dependsOnOf({ dependsOn: raw });
|
|
15493
|
+
if (ids.length === 0) return { ok: true, ids: [] };
|
|
15494
|
+
if (specId && ids.includes(specId))
|
|
15495
|
+
return { ok: false, error: "backlog tak bisa bergantung pada dirinya sendiri" };
|
|
15496
|
+
const rows = await prisma.spec.findMany({
|
|
15497
|
+
where: { id: { in: ids } },
|
|
15498
|
+
select: { id: true, projectId: true }
|
|
15499
|
+
});
|
|
15500
|
+
const found = new Map(rows.map((r) => [r.id, r.projectId]));
|
|
15501
|
+
const missing = ids.filter((i) => !found.has(i));
|
|
15502
|
+
if (missing.length) return { ok: false, error: `backlog tak ditemukan: ${missing.join(", ")}` };
|
|
15503
|
+
const foreign = ids.filter((i) => found.get(i) !== projectId);
|
|
15504
|
+
if (foreign.length)
|
|
15505
|
+
return { ok: false, error: `dependency harus di project yang sama: ${foreign.join(", ")}` };
|
|
15506
|
+
if (specId) {
|
|
15507
|
+
const all = await prisma.spec.findMany({
|
|
15508
|
+
where: { projectId },
|
|
15509
|
+
select: { id: true, dependsOn: true }
|
|
15510
|
+
});
|
|
15511
|
+
const edges = new Map(all.map((r) => [r.id, dependsOnOf(r)]));
|
|
15512
|
+
edges.set(specId, ids);
|
|
15513
|
+
if (reaches(edges, ids, specId))
|
|
15514
|
+
return { ok: false, error: "dependency membentuk siklus" };
|
|
15515
|
+
}
|
|
15516
|
+
return { ok: true, ids };
|
|
15517
|
+
}
|
|
15518
|
+
|
|
15363
15519
|
// src/services/doc-export.ts
|
|
15364
15520
|
import PDFDocument from "pdfkit";
|
|
15365
15521
|
|
|
@@ -17829,7 +17985,7 @@ async function liveSpecs(filter = {}) {
|
|
|
17829
17985
|
orderBy: { id: "desc" }
|
|
17830
17986
|
});
|
|
17831
17987
|
const live = sessionPhasesBySpec();
|
|
17832
|
-
if (live.size === 0) return specs;
|
|
17988
|
+
if (live.size === 0) return decorateBlocked(specs);
|
|
17833
17989
|
const advanced = [];
|
|
17834
17990
|
const doneNow = [];
|
|
17835
17991
|
const out3 = specs.map((s) => {
|
|
@@ -17847,7 +18003,7 @@ async function liveSpecs(filter = {}) {
|
|
|
17847
18003
|
if (res.count > 0) await notifySynced("spec", a.id);
|
|
17848
18004
|
}));
|
|
17849
18005
|
await Promise.all(doneNow.map((d) => recordCompletion(d.specId, d.title, d.projectId)));
|
|
17850
|
-
return out3;
|
|
18006
|
+
return decorateBlocked(out3);
|
|
17851
18007
|
}
|
|
17852
18008
|
|
|
17853
18009
|
// src/routes/specs.ts
|
|
@@ -17887,6 +18043,8 @@ async function specs_default(app2) {
|
|
|
17887
18043
|
const repoDir = await resolveRepoDir(b.project);
|
|
17888
18044
|
if (b.branchFrom && await branchUnknown(repoDir, b.branchFrom))
|
|
17889
18045
|
return reply.code(400).send({ error: `branch "${b.branchFrom}" tidak ada di repo project` });
|
|
18046
|
+
const dep = await validateDependsOn(null, b.project, b.dependsOn ?? []);
|
|
18047
|
+
if (!dep.ok) return reply.code(400).send({ error: dep.error });
|
|
17890
18048
|
const isQa = b.source === "qa";
|
|
17891
18049
|
const { priority, objective } = deriveSpecFields(b.source, b.payload, b.priority);
|
|
17892
18050
|
const author = req.user?.email ?? "system";
|
|
@@ -17905,7 +18063,9 @@ async function specs_default(app2) {
|
|
|
17905
18063
|
author: isQa ? `QA \xB7 ${author}` : b.source === "audit" ? `Audit \xB7 ${author}` : b.source === "goal" ? `Goal \xB7 ${author}` : author,
|
|
17906
18064
|
objective,
|
|
17907
18065
|
payload: b.payload,
|
|
17908
|
-
branchFrom: b.branchFrom ?? null
|
|
18066
|
+
branchFrom: b.branchFrom ?? null,
|
|
18067
|
+
dependsOn: dep.ids
|
|
18068
|
+
// SPEC-447 · ADR-0093
|
|
17909
18069
|
}
|
|
17910
18070
|
});
|
|
17911
18071
|
} catch (e) {
|
|
@@ -17969,7 +18129,7 @@ ${item.context}` : item.context;
|
|
|
17969
18129
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
17970
18130
|
const spec = await prisma.spec.findUnique({ where: { id } });
|
|
17971
18131
|
if (!spec) return reply.code(404).send({ error: "not found" });
|
|
17972
|
-
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload } = parsed.data;
|
|
18132
|
+
const { branchFrom, stage, confirmDelete, title, priority: newPriority, payload, dependsOn } = parsed.data;
|
|
17973
18133
|
const editingContent = title !== void 0 || newPriority !== void 0 || payload !== void 0;
|
|
17974
18134
|
if (editingContent && (spec.stage !== "brainstorming" || spec.baseSha !== null))
|
|
17975
18135
|
return reply.code(409).send({ error: "backlog item sudah dimulai \u2014 tak bisa diedit" });
|
|
@@ -17977,6 +18137,12 @@ ${item.context}` : item.context;
|
|
|
17977
18137
|
if (await branchUnknown(await resolveRepoDir(spec.projectId), branchFrom))
|
|
17978
18138
|
return reply.code(400).send({ error: `branch "${branchFrom}" tidak ada di repo project` });
|
|
17979
18139
|
}
|
|
18140
|
+
let depIds;
|
|
18141
|
+
if (dependsOn !== void 0) {
|
|
18142
|
+
const d = await validateDependsOn(spec.id, spec.projectId, dependsOn);
|
|
18143
|
+
if (!d.ok) return reply.code(400).send({ error: d.error });
|
|
18144
|
+
depIds = d.ids;
|
|
18145
|
+
}
|
|
17980
18146
|
if (stage !== void 0) {
|
|
17981
18147
|
if (STAGES.indexOf(stage) >= STAGES.indexOf(spec.stage))
|
|
17982
18148
|
return reply.code(422).send({ error: "stage hanya boleh dikembalikan mundur" });
|
|
@@ -17988,6 +18154,7 @@ ${item.context}` : item.context;
|
|
|
17988
18154
|
}
|
|
17989
18155
|
const data = {};
|
|
17990
18156
|
if (branchFrom !== void 0) data.branchFrom = branchFrom;
|
|
18157
|
+
if (depIds !== void 0) data.dependsOn = depIds;
|
|
17991
18158
|
if (stage !== void 0) data.stage = stage;
|
|
17992
18159
|
if (editingContent) {
|
|
17993
18160
|
const effPayload = payload ?? spec.payload;
|
|
@@ -18020,8 +18187,21 @@ ${item.context}` : item.context;
|
|
|
18020
18187
|
});
|
|
18021
18188
|
app2.delete("/specs/:id", async (req, reply) => {
|
|
18022
18189
|
const { id } = req.params;
|
|
18190
|
+
const gone = await prisma.spec.findUnique({ where: { id }, select: { projectId: true } });
|
|
18023
18191
|
await prisma.spec.delete({ where: { id } }).catch(() => {
|
|
18024
18192
|
});
|
|
18193
|
+
if (gone) {
|
|
18194
|
+
const rows = await prisma.spec.findMany({
|
|
18195
|
+
where: { projectId: gone.projectId },
|
|
18196
|
+
select: { id: true, dependsOn: true }
|
|
18197
|
+
});
|
|
18198
|
+
for (const r of rows) {
|
|
18199
|
+
const ids = dependsOnOf(r);
|
|
18200
|
+
if (!ids.includes(id)) continue;
|
|
18201
|
+
await prisma.spec.update({ where: { id: r.id }, data: { dependsOn: ids.filter((x) => x !== id) } });
|
|
18202
|
+
await notifySynced("spec", r.id);
|
|
18203
|
+
}
|
|
18204
|
+
}
|
|
18025
18205
|
return reply.code(204).send();
|
|
18026
18206
|
});
|
|
18027
18207
|
app2.post("/specs/:id/integrate", async (req, reply) => {
|
|
@@ -19251,9 +19431,11 @@ init_pty();
|
|
|
19251
19431
|
init_session_phases();
|
|
19252
19432
|
init_pty();
|
|
19253
19433
|
var LaunchError = class extends Error {
|
|
19254
|
-
|
|
19434
|
+
// SPEC-447 · `blockers` hanya terisi untuk kind "blocked"; route memetakannya ke body 409.
|
|
19435
|
+
constructor(message, kind, blockers = []) {
|
|
19255
19436
|
super(message);
|
|
19256
19437
|
this.kind = kind;
|
|
19438
|
+
this.blockers = blockers;
|
|
19257
19439
|
}
|
|
19258
19440
|
};
|
|
19259
19441
|
function resumeState(repoDir, worktree, branchTo, headSha) {
|
|
@@ -19275,6 +19457,11 @@ async function startSpecSession(spec, opts) {
|
|
|
19275
19457
|
const id = sessionIdForSpec(spec.id);
|
|
19276
19458
|
const pane = getSession(id);
|
|
19277
19459
|
if (pane && !pane.exited) return { id: pane.id, reused: true };
|
|
19460
|
+
if (!opts.force) {
|
|
19461
|
+
const blockers = await blockersForSpec(spec, repoDir);
|
|
19462
|
+
if (blockers.length)
|
|
19463
|
+
throw new LaunchError(`${spec.id} ${blockedNote(blockers)}`, "blocked", blockers);
|
|
19464
|
+
}
|
|
19278
19465
|
if (pane) killSession(id);
|
|
19279
19466
|
const setting = await getSetting();
|
|
19280
19467
|
const agent = opts.agent ?? setting.agent;
|
|
@@ -19437,12 +19624,16 @@ async function terminal_default(app2) {
|
|
|
19437
19624
|
// SPEC-332 · ADR-0073
|
|
19438
19625
|
agent: parsed.data.agent,
|
|
19439
19626
|
// SPEC-338 · ADR-0074
|
|
19440
|
-
verifyScope: parsed.data.verifyScope
|
|
19627
|
+
verifyScope: parsed.data.verifyScope,
|
|
19441
19628
|
// SPEC-376 · ADR-0080
|
|
19629
|
+
force: parsed.data.force
|
|
19630
|
+
// SPEC-447 · ADR-0093
|
|
19442
19631
|
});
|
|
19443
19632
|
return reply.code(201).send(r.resumed ? { id: r.id, resumed: true } : { id: r.id });
|
|
19444
19633
|
} catch (e) {
|
|
19445
19634
|
if (e instanceof LaunchError) {
|
|
19635
|
+
if (e.kind === "blocked")
|
|
19636
|
+
return reply.code(409).send({ error: e.message, blocked: true, blockers: e.blockers });
|
|
19446
19637
|
return e.kind === "needs-bind" ? reply.code(400).send({ error: e.message, needsBind: true }) : reply.code(422).send({ error: e.message });
|
|
19447
19638
|
}
|
|
19448
19639
|
throw e;
|
|
@@ -20378,7 +20569,7 @@ import { readFileSync as readFileSync9 } from "node:fs";
|
|
|
20378
20569
|
import { homedir as homedir6 } from "node:os";
|
|
20379
20570
|
import { join as join9 } from "node:path";
|
|
20380
20571
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
20381
|
-
var
|
|
20572
|
+
var TTL_MS2 = 3e4;
|
|
20382
20573
|
var lastOk = null;
|
|
20383
20574
|
var freshUntil = 0;
|
|
20384
20575
|
var LABELS = {
|
|
@@ -20456,7 +20647,7 @@ async function fetchUsage() {
|
|
|
20456
20647
|
async function getLimits() {
|
|
20457
20648
|
if (lastOk && Date.now() < freshUntil) return lastOk;
|
|
20458
20649
|
const dto = await fetchUsage();
|
|
20459
|
-
if (dto.status === "ok") freshUntil = Date.now() +
|
|
20650
|
+
if (dto.status === "ok") freshUntil = Date.now() + TTL_MS2;
|
|
20460
20651
|
return dto;
|
|
20461
20652
|
}
|
|
20462
20653
|
|
|
@@ -20464,7 +20655,7 @@ async function getLimits() {
|
|
|
20464
20655
|
import { open as open2, readdir as readdir2, stat } from "node:fs/promises";
|
|
20465
20656
|
import { homedir as homedir7 } from "node:os";
|
|
20466
20657
|
import { join as join10 } from "node:path";
|
|
20467
|
-
var
|
|
20658
|
+
var TTL_MS3 = 3e4;
|
|
20468
20659
|
var STALE_AFTER_MS = 12 * 36e5;
|
|
20469
20660
|
var TAIL_BYTES = 512 * 1024;
|
|
20470
20661
|
var MAX_FILES = 8;
|
|
@@ -20569,7 +20760,7 @@ async function getCodexLimits(dir2) {
|
|
|
20569
20760
|
const dto = await readLimits(codexSessionsDir());
|
|
20570
20761
|
if (dto.status !== "unavailable") {
|
|
20571
20762
|
cache2 = dto;
|
|
20572
|
-
freshUntil2 = Date.now() +
|
|
20763
|
+
freshUntil2 = Date.now() + TTL_MS3;
|
|
20573
20764
|
}
|
|
20574
20765
|
return dto;
|
|
20575
20766
|
}
|
|
@@ -20587,7 +20778,7 @@ import { execFile as execFile8 } from "node:child_process";
|
|
|
20587
20778
|
import { promisify as promisify8 } from "node:util";
|
|
20588
20779
|
var run = promisify8(execFile8);
|
|
20589
20780
|
var CODEX_MIN_CLIENT = "0.144.0";
|
|
20590
|
-
var
|
|
20781
|
+
var TTL_MS4 = 5 * 6e4;
|
|
20591
20782
|
var cache3 = null;
|
|
20592
20783
|
var codexBin2 = () => effectiveStr("HANOMAN_CODEX_BIN") ?? "codex";
|
|
20593
20784
|
function parseCodexVersion(out3) {
|
|
@@ -20595,7 +20786,7 @@ function parseCodexVersion(out3) {
|
|
|
20595
20786
|
return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
|
|
20596
20787
|
}
|
|
20597
20788
|
async function getCodexVersion(now = Date.now()) {
|
|
20598
|
-
if (cache3 && now - cache3.at <
|
|
20789
|
+
if (cache3 && now - cache3.at < TTL_MS4) return cache3.version;
|
|
20599
20790
|
let version = null;
|
|
20600
20791
|
try {
|
|
20601
20792
|
const { stdout } = await run(codexBin2(), ["--version"], { timeout: 5e3 });
|
|
@@ -21825,6 +22016,11 @@ async function markDone(id, note) {
|
|
|
21825
22016
|
data: { status: "done", ...note ? { note } : {} }
|
|
21826
22017
|
});
|
|
21827
22018
|
}
|
|
22019
|
+
async function noteQueued(id, note) {
|
|
22020
|
+
const row = await prisma.schedulerQueueItem.findUnique({ where: { id }, select: { note: true } });
|
|
22021
|
+
if (row?.note === note) return;
|
|
22022
|
+
await prisma.schedulerQueueItem.update({ where: { id }, data: { note } });
|
|
22023
|
+
}
|
|
21828
22024
|
|
|
21829
22025
|
// src/services/scheduler/registry.ts
|
|
21830
22026
|
var sources = /* @__PURE__ */ new Map();
|
|
@@ -22113,6 +22309,7 @@ function toDecisionView(r) {
|
|
|
22113
22309
|
|
|
22114
22310
|
// src/services/lead/brain.ts
|
|
22115
22311
|
init_config2();
|
|
22312
|
+
init_pty();
|
|
22116
22313
|
import { execFile as execFile9 } from "node:child_process";
|
|
22117
22314
|
var binFor = (agent) => agent === "codex" ? effectiveStr("HANOMAN_CODEX_BIN") ?? "codex" : effectiveStr("HANOMAN_CLAUDE_BIN") ?? "claude";
|
|
22118
22315
|
function leadArgv(o) {
|
|
@@ -22133,14 +22330,16 @@ function leadArgv(o) {
|
|
|
22133
22330
|
o.prompt
|
|
22134
22331
|
];
|
|
22135
22332
|
}
|
|
22333
|
+
var leadEnv = (agent, base = process.env, uid = process.getuid?.()) => agent === "claude" ? { ...rootBypassEnv(uid), ...base } : { ...base };
|
|
22136
22334
|
function think(prompt, o) {
|
|
22137
22335
|
const bin = binFor(o.agent);
|
|
22138
22336
|
const args = leadArgv({ agent: o.agent, model: o.model, effort: o.effort, prompt });
|
|
22139
22337
|
return new Promise((resolve14, reject) => {
|
|
22140
|
-
execFile9(bin, args, {
|
|
22338
|
+
const child = execFile9(bin, args, {
|
|
22141
22339
|
cwd: o.cwd,
|
|
22142
22340
|
timeout: o.timeoutMs,
|
|
22143
22341
|
maxBuffer: 16 * 1024 * 1024,
|
|
22342
|
+
env: leadEnv(o.agent),
|
|
22144
22343
|
encoding: "utf8",
|
|
22145
22344
|
killSignal: "SIGTERM"
|
|
22146
22345
|
}, (err, stdout, stderr) => {
|
|
@@ -22151,6 +22350,7 @@ function think(prompt, o) {
|
|
|
22151
22350
|
}
|
|
22152
22351
|
resolve14(stdout);
|
|
22153
22352
|
});
|
|
22353
|
+
child.stdin?.end();
|
|
22154
22354
|
});
|
|
22155
22355
|
}
|
|
22156
22356
|
|
|
@@ -22736,11 +22936,22 @@ async function orderReadyWork(optIn, deps) {
|
|
|
22736
22936
|
async function orderProject(projectId, deps) {
|
|
22737
22937
|
const project = await prisma.project.findUnique({ where: { id: projectId }, select: { schedulerOptIn: true } });
|
|
22738
22938
|
if (!project?.schedulerOptIn) return 0;
|
|
22739
|
-
const
|
|
22939
|
+
const readyRaw = await prisma.spec.findMany({
|
|
22740
22940
|
where: { ...UNSTARTED_SPEC_WHERE, projectId },
|
|
22741
|
-
select: {
|
|
22941
|
+
select: {
|
|
22942
|
+
id: true,
|
|
22943
|
+
projectId: true,
|
|
22944
|
+
title: true,
|
|
22945
|
+
priority: true,
|
|
22946
|
+
objective: true,
|
|
22947
|
+
branchFrom: true,
|
|
22948
|
+
dependsOn: true
|
|
22949
|
+
},
|
|
22742
22950
|
orderBy: { id: "asc" }
|
|
22743
22951
|
});
|
|
22952
|
+
const repoDir = await resolveRepoDir(projectId);
|
|
22953
|
+
const ready = [];
|
|
22954
|
+
for (const r of readyRaw) if ((await blockersForSpec(r, repoDir)).length === 0) ready.push(r);
|
|
22744
22955
|
if (ready.length < 2) return 0;
|
|
22745
22956
|
const already = new Set((await prisma.schedulerQueueItem.findMany({
|
|
22746
22957
|
where: { specId: { in: ready.map((r) => r.id) } },
|
|
@@ -23407,6 +23618,11 @@ async function drain(cfg, deps) {
|
|
|
23407
23618
|
await markDone(item.id, ALREADY_DONE_NOTE);
|
|
23408
23619
|
continue;
|
|
23409
23620
|
}
|
|
23621
|
+
const blocked = await deps.blockers(item.specId);
|
|
23622
|
+
if (blocked.length) {
|
|
23623
|
+
await noteQueued(item.id, blockedNote(blocked));
|
|
23624
|
+
continue;
|
|
23625
|
+
}
|
|
23410
23626
|
const liveId = deps.isLive(item.specId);
|
|
23411
23627
|
if (liveId) {
|
|
23412
23628
|
await markLaunched(item.id, liveId);
|
|
@@ -23536,6 +23752,13 @@ var prodDeps = {
|
|
|
23536
23752
|
const s = await prisma.spec.findUnique({ where: { id: specId }, select: { stage: true } });
|
|
23537
23753
|
return s?.stage === "done";
|
|
23538
23754
|
},
|
|
23755
|
+
// SPEC-447 · ADR-0093 · dibaca ULANG dari DB tepat sebelum launch, seperti `isDone`: `dependsOn`
|
|
23756
|
+
// bisa ditulis operator selagi item mengantre, dan merged-ness bergerak sendiri saat ada integrate.
|
|
23757
|
+
blockers: async (specId) => {
|
|
23758
|
+
const spec = await prisma.spec.findUnique({ where: { id: specId } });
|
|
23759
|
+
if (!spec) return [];
|
|
23760
|
+
return blockersForSpec(spec, await resolveRepoDir(spec.projectId));
|
|
23761
|
+
},
|
|
23539
23762
|
launch: async (item, autonomy) => {
|
|
23540
23763
|
const spec = await prisma.spec.findUnique({ where: { id: item.specId } });
|
|
23541
23764
|
if (!spec) throw new Error(`spec ${item.specId} tak ada`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- SPEC-447 · ADR-0093 · dependency antar-backlog.
|
|
2
|
+
--
|
|
3
|
+
-- ADITIF & nullable tanpa default → `ADD COLUMN` polos sudah cukup. Beda dari migration SPEC-408
|
|
4
|
+
-- yang harus meredefinisi tabel: yang dilarang SQLite adalah `ADD COLUMN … DEFAULT <non-konstan>`,
|
|
5
|
+
-- bukan `ADD COLUMN` itu sendiri. Baris lama tetap NULL, dan pembaca menerjemahkan NULL → []
|
|
6
|
+
-- (`dependsOnOf` di services/spec-deps.ts) — jadi tak ada backfill yang perlu dilakukan.
|
|
7
|
+
ALTER TABLE "Spec" ADD COLUMN "dependsOn" JSONB;
|
package/prisma/schema.prisma
CHANGED
|
@@ -46,6 +46,12 @@ model Spec {
|
|
|
46
46
|
// sama dengan `baseSha` (services/session-launch.ts); jalur RESUME (ADR-0084) sengaja tidak
|
|
47
47
|
// menimpanya — melanjutkan sesi bukan "mulai lagi". null = belum pernah dikerjakan.
|
|
48
48
|
startedAt DateTime?
|
|
49
|
+
// SPEC-447 · ADR-0093 · id spec lain yang harus SELESAI & commit-nya sudah ada di branch basis
|
|
50
|
+
// sebelum item ini boleh diluncurkan. Array JSON of string; null/[] = tak bergantung apa pun.
|
|
51
|
+
// Sengaja kolom, bukan tabel join: SQLite melarang scalar list, `Json` sudah dipakai `payload`,
|
|
52
|
+
// dan kolom ikut FIELDS.spec sync apa adanya. Integritas ditegakkan di boundary route +
|
|
53
|
+
// pembersihan saat spec dihapus, bukan FK.
|
|
54
|
+
dependsOn Json?
|
|
49
55
|
updatedAt DateTime @updatedAt
|
|
50
56
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
51
57
|
}
|