@staix/agent-hub 0.12.44 → 0.12.45
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/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +102 -14
- package/docs/agent-notes/README.md +9 -0
- package/docs/agent-notes/adapters.md +58 -44
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/daemon.md +9 -7
- package/docs/agent-notes/local-worker.md +4 -3
- package/docs/agent-notes/tasks.md +8 -5
- package/docs/agent-notes/tests.md +7 -7
- package/docs/operations.md +99 -7
- package/docs/quickstart.md +5 -1
- package/docs/sdlc.md +26 -8
- package/docs/security.md +6 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +114 -5
- package/docs/verification/2026-10-21-469-r1-dashboard-tokens.md +43 -0
- package/docs/verification/2026-10-21-469-r2-dashboard-shell.md +54 -0
- package/docs/verification/2026-10-21-469-r3-dashboard-board.md +51 -0
- package/docs/verification/2026-10-21-469-r4-dashboard-views.md +77 -0
- package/docs/verification/2026-10-21-469-r6-dashboard-motion-states.md +82 -0
- package/docs/verification/2026-10-22-0.12.45.md +60 -0
- package/docs/verification/assets/469-r1-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r1-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-agents-768-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1024-light.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r2-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-board-768-light.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-board-390-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-390-dark.png +0 -0
- package/docs/verification/assets/469-r3-plan-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-plan-390-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-memory-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-message-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-390-light.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-light.png +0 -0
- package/docs/verification/assets/469-r6-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-toasts-390-light.png +0 -0
- package/docs/verified.json +19 -16
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +9818 -9605
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +3 -1
- package/src/adapters/claude-channel.ts +14 -5
- package/src/adapters/codex-appserver.ts +1 -0
- package/src/adapters/conformance/acp.ts +114 -0
- package/src/adapters/conformance/codex.ts +191 -0
- package/src/adapters/conformance/enrollment.ts +43 -0
- package/src/adapters/conformance/factories.ts +17 -0
- package/src/adapters/conformance/fake-paths.ts +12 -0
- package/src/adapters/conformance/fakes/acp-server.ts +267 -0
- package/src/adapters/conformance/fakes/acp-watchdog-agent.ts +10 -0
- package/src/adapters/conformance/fakes/app-server.ts +119 -0
- package/src/adapters/conformance/fakes/codex-bin.ts +15 -0
- package/src/adapters/conformance/fakes/jcode-agent.ts +91 -0
- package/src/adapters/conformance/fakes/model-server.ts +39 -0
- package/src/adapters/conformance/fakes/pi-rpc.ts +32 -0
- package/src/adapters/conformance/index.ts +11 -0
- package/src/adapters/conformance/kit.ts +50 -0
- package/src/adapters/conformance/local-pi.ts +100 -0
- package/src/adapters/conformance/owned-acp.ts +44 -0
- package/src/adapters/definitions/builtin-metadata.ts +23 -6
- package/src/adapters/definitions/native-cli-entry-kimi.ts +7 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/qwen.ts +4 -1
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-module-scan.ts +126 -0
- package/src/adapters/external-modules.ts +240 -0
- package/src/adapters/pi.ts +2 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +10 -0
- package/src/adapters/registry.ts +51 -15
- package/src/cli/help.ts +21 -16
- package/src/cli/identity.ts +1 -1
- package/src/cli/main.ts +23 -5
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +3 -2
- package/src/cli/upgrade-runtime.ts +4 -1
- package/src/hub/board.ts +30 -4
- package/src/hub/config-apply.ts +18 -10
- package/src/hub/config-reload.ts +8 -5
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +95 -39
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +21 -5
- package/src/hub/instance-config.ts +17 -5
- package/src/hub/instance-execution.ts +20 -8
- package/src/hub/instances.ts +2 -1
- package/src/hub/peers.ts +9 -0
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +31 -5
- package/src/hub/sdlc-queue.ts +19 -4
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +27 -12
- package/src/hub/settings.ts +5 -1
- package/src/hub/snapshots.ts +8 -6
- package/src/hub/tasks.ts +275 -45
- package/src/hub/ui.ts +2 -2
- package/src/ui/index.html +559 -78
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +44 -0
package/src/hub/sdlc.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { planConformanceBlockers, type PlanConformance } from "./plan-conformance.ts";
|
|
2
|
+
import { DEFAULT_MAX_NITS, findingsForHead, findingHead, findingTally, type ReviewFindingSet, type ReviewFindingTally } from "./review-findings.ts";
|
|
1
3
|
import type { CostPlan } from "./cost-governance.ts";
|
|
2
4
|
import { criteriaReceipt, type CriteriaReceipt, type CriteriaView, type LegacyPhaseReceipt } from "./workflow.ts";
|
|
3
5
|
import { constants, openSync, fstatSync, readSync, closeSync } from "node:fs";
|
|
@@ -29,7 +31,7 @@ export type StageGate = "issue" | "pr" | "local" | "review" | "checks" | "criter
|
|
|
29
31
|
export interface StageDefinition { base: SdlcBaseStage; actor: string; entry_gates: StageGate[]; exit_gates: StageGate[]; after: string[]; }
|
|
30
32
|
export type SdlcMode = "off" | "advise" | "enforce";
|
|
31
33
|
/** `workflow: "gsd-lite"` marks the retired setting during its window (#481 S8): every task then needs a criteria checklist. */
|
|
32
|
-
export interface SdlcConfig { workflow?: "gsd-lite"; mode: SdlcMode; requiredChecks: string[]; stages: SdlcStage[]; definitions?: Record<string, StageDefinition>; deprecations?: string[]; }
|
|
34
|
+
export interface SdlcConfig { workflow?: "gsd-lite"; maxNits?: number; mode: SdlcMode; requiredChecks: string[]; stages: SdlcStage[]; definitions?: Record<string, StageDefinition>; deprecations?: string[]; }
|
|
33
35
|
export interface PlanReviewReceipt {
|
|
34
36
|
planner: string;
|
|
35
37
|
planningRevision: number;
|
|
@@ -63,6 +65,8 @@ export interface PrEvidence {
|
|
|
63
65
|
pendingReviews: number | null;
|
|
64
66
|
paths: string[];
|
|
65
67
|
pathsComplete?: boolean;
|
|
68
|
+
/** All affected path identities are represented; rename/copy destinations alone cannot certify S2. */
|
|
69
|
+
typesComplete?: boolean;
|
|
66
70
|
draft?: boolean;
|
|
67
71
|
base?: string;
|
|
68
72
|
baseHead?: string;
|
|
@@ -125,7 +129,10 @@ export function sdlcConfig(value: unknown, version?: string): SdlcConfig {
|
|
|
125
129
|
if (value === undefined) return { mode: "off", requiredChecks: [], stages: [...SDLC_STAGES] };
|
|
126
130
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("sdlc: expected a table");
|
|
127
131
|
const raw = value as Record<string, unknown>;
|
|
128
|
-
for (const key of Object.keys(raw)) if (!["mode", "required_checks", "stages", "workflow", "definitions"].includes(key)) throw new Error(`sdlc.${key}: unknown field`);
|
|
132
|
+
for (const key of Object.keys(raw)) if (!["mode", "required_checks", "stages", "workflow", "definitions", "max_nits"].includes(key)) throw new Error(`sdlc.${key}: unknown field`);
|
|
133
|
+
const maxNits = raw.max_nits === undefined ? DEFAULT_MAX_NITS : raw.max_nits;
|
|
134
|
+
if (typeof maxNits !== "number" || !Number.isSafeInteger(maxNits) || maxNits < 0 || maxNits > 64) throw new Error("sdlc.max_nits: expected an integer from 0 to 64");
|
|
135
|
+
const findingConfig = raw.max_nits === undefined ? {} : { maxNits };
|
|
129
136
|
const mode = raw.mode ?? "off";
|
|
130
137
|
if (mode !== "off" && mode !== "advise" && mode !== "enforce") throw new Error("sdlc.mode: expected off, advise or enforce");
|
|
131
138
|
// ponytail: the #481 S8 window is a fixed version; delete `workflow`, the phase aliases and their tests once it passes.
|
|
@@ -140,7 +147,7 @@ export function sdlcConfig(value: unknown, version?: string): SdlcConfig {
|
|
|
140
147
|
if (raw.definitions !== undefined) {
|
|
141
148
|
const custom = definitions(raw.definitions, raw.stages, phaseAlias);
|
|
142
149
|
if (phaseAlias.used) notes.push(`the sdlc phase gate is deprecated and stops loading in ${RETIRED_WORKFLOW_FROM} (#481): name it criteria`);
|
|
143
|
-
return { mode, requiredChecks: [...checks] as string[], ...custom, ...(workflow === "gsd-lite" ? { workflow } : {}), ...(notes.length ? { deprecations: notes } : {}) };
|
|
150
|
+
return { mode, ...findingConfig, requiredChecks: [...checks] as string[], ...custom, ...(workflow === "gsd-lite" ? { workflow } : {}), ...(notes.length ? { deprecations: notes } : {}) };
|
|
144
151
|
}
|
|
145
152
|
// Off keeps the #257 seven stages byte for byte; an enabled lifecycle defaults to the playbook (#481 S0).
|
|
146
153
|
const given = raw.stages ?? (mode === "off" ? [...SDLC_STAGES] : [...PLAYBOOK_STAGES]);
|
|
@@ -148,7 +155,7 @@ export function sdlcConfig(value: unknown, version?: string): SdlcConfig {
|
|
|
148
155
|
// An enabled #257 list keeps its order under canonical ids, so implement/verify stay read-only aliases there too.
|
|
149
156
|
const stages = mode !== "off" && exactly(SDLC_STAGES) ? SDLC_STAGES.map(canonicalStage) : given;
|
|
150
157
|
if (!exactly(SDLC_STAGES) && !exactly(PLAYBOOK_STAGES)) throw new Error(`sdlc.stages: use ${PLAYBOOK_STAGES.join(", ")} in that order (or the #257 list ${SDLC_STAGES.join(", ")}); other lists need definitions`);
|
|
151
|
-
return { mode, requiredChecks: [...checks] as string[], stages: [...stages as string[]], ...(workflow === "gsd-lite" ? { workflow } : {}), ...(notes.length ? { deprecations: notes } : {}) };
|
|
158
|
+
return { mode, ...findingConfig, requiredChecks: [...checks] as string[], stages: [...stages as string[]], ...(workflow === "gsd-lite" ? { workflow } : {}), ...(notes.length ? { deprecations: notes } : {}) };
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
export function loadSdlc(cwd: string, version?: string): SdlcConfig {
|
|
@@ -216,7 +223,9 @@ export function parsePrEvidence(value: unknown): PrEvidence {
|
|
|
216
223
|
return { name, state } as PrEvidence["checks"][number];
|
|
217
224
|
});
|
|
218
225
|
const paths = p.files.map((f: Record<string, unknown>) => { if (typeof f.path !== "string" || !f.path || f.path.length > 1000) throw new Error("PR readback: invalid file identity"); return f.path; });
|
|
219
|
-
|
|
226
|
+
const completeTypes = p.files.every((file: Record<string, unknown>) => typeof file.changeType === "string" && ["ADDED", "DELETED", "MODIFIED", "CHANGED"].includes(file.changeType));
|
|
227
|
+
// Rename/copy metadata exposes only its destination, never a complete inventory of affected identities.
|
|
228
|
+
return { head: p.headRefOid, state: p.state, checks, paths, pathsComplete: Number.isSafeInteger(p.changedFiles) && p.changedFiles >= 0 && p.changedFiles === paths.length, typesComplete: completeTypes, ...(typeof p.isDraft === "boolean" ? { draft: p.isDraft } : {}), ...(typeof p.baseRefName === "string" && p.baseRefName && p.baseRefName.length <= 200 && !/[\x00-\x1f\x7f]/.test(p.baseRefName) ? { base: p.baseRefName } : {}), ...(sha(p.baseRefOid) ? { baseHead: p.baseRefOid } : {}),
|
|
220
229
|
review: p.reviewDecision === "APPROVED" ? "approved" : p.reviewDecision === "CHANGES_REQUESTED" ? "changes_requested" : "unknown",
|
|
221
230
|
pendingReviews: Array.isArray(p.reviewRequests) && p.reviewRequests.length <= 100 && p.reviewRequests.every((r: unknown) => !!r && typeof r === "object") ? p.reviewRequests.length : null,
|
|
222
231
|
unresolved: Number.isSafeInteger(p.unresolvedThreads) && p.unresolvedThreads >= 0 ? p.unresolvedThreads : null,
|
|
@@ -239,12 +248,16 @@ export const SPEC_STATE_TEXT: Record<Exclude<SpecState, "current">, string> = {
|
|
|
239
248
|
changed: "spec changed after acceptance: the person re-accepts with ahub sdlc accept <task> --issue <n>",
|
|
240
249
|
unknown: "spec edit time unknown: the issue could not be read",
|
|
241
250
|
};
|
|
242
|
-
export interface SdlcStatus { spec?: { issue: number; state: SpecState }; playbook?: string; criteria?: CriteriaView; mode: SdlcMode; stage: SdlcStage; definition?: StageDefinition; entry?: string[]; exit?: string[]; blockers: string[]; ready: boolean; actor: string; next: string; }
|
|
251
|
+
export interface SdlcStatus { planConformance?: PlanConformance & { accepted?: boolean; earlierPushes?: "unknown"; revisionsAfterFirstObservedPush?: number }; findings?: ReviewFindingTally; spec?: { issue: number; state: SpecState }; playbook?: string; criteria?: CriteriaView; mode: SdlcMode; stage: SdlcStage; definition?: StageDefinition; entry?: string[]; exit?: string[]; blockers: string[]; ready: boolean; actor: string; next: string; }
|
|
243
252
|
/** The acceptance-criteria checklist (#481 S8, Compliance pass) counts once recorded, and on every task under the retired gsd-lite setting. Off keeps #257's output. */
|
|
244
253
|
export const criteriaRequired = (config: SdlcConfig, receipt: SdlcReceipt | undefined): boolean => config.workflow === "gsd-lite" || (config.mode !== "off" && !!checklistOf(receipt));
|
|
245
|
-
export function sdlcStatus(config: SdlcConfig, receipt: SdlcReceipt | undefined, evidence?: PrEvidence, criteria?: CriteriaView, specEdit?: SpecEdit): SdlcStatus {
|
|
254
|
+
export function sdlcStatus(config: SdlcConfig, receipt: SdlcReceipt | undefined, evidence?: PrEvidence, criteria?: CriteriaView, specEdit?: SpecEdit, sets: readonly ReviewFindingSet[] = [], commit?: string, plan?: SdlcStatus["planConformance"]): SdlcStatus {
|
|
246
255
|
const stage = receipt?.stage ?? config.stages[0] ?? "spec", blockers: string[] = [];
|
|
247
256
|
const definition = stageDefinition(config, stage);
|
|
257
|
+
const items = findingsForHead(sets, findingHead(receipt, evidence, commit));
|
|
258
|
+
const findings = items.length ? findingTally(items) : undefined;
|
|
259
|
+
if (config.mode === "enforce" && findings?.openImportant) blockers.push("open important review findings: " + findings.openImportant);
|
|
260
|
+
if (config.mode === "enforce") blockers.push(...planConformanceBlockers(plan));
|
|
248
261
|
if (!definition) blockers.push("lifecycle stage definition unknown");
|
|
249
262
|
if (!receipt?.repo || !receipt.issue) blockers.push("issue identity missing");
|
|
250
263
|
if (!receipt?.pr) blockers.push("PR identity missing");
|
|
@@ -270,7 +283,7 @@ export function sdlcStatus(config: SdlcConfig, receipt: SdlcReceipt | undefined,
|
|
|
270
283
|
const shown = criteria && (criteriaRequired(config, receipt) || definition && [...definition.entry_gates, ...definition.exit_gates].includes("criteria"));
|
|
271
284
|
// Shown, not counted as a readiness blocker: the spec gate is build entry (#481 S1).
|
|
272
285
|
const spec = config.mode !== "off" && receipt?.issue ? { spec: { issue: receipt.issue, state: specState(receipt, specEdit) } } : {};
|
|
273
|
-
return { ...spec, ...(config.mode !== "off" && definition && playbookStage(definition.base) ? { playbook: playbookStage(definition.base) } : {}), ...(shown ? { criteria } : {}), ...(custom ? { definition, ...custom } : {}), mode: config.mode, stage, blockers, ready: blockers.length === 0,
|
|
286
|
+
return { ...spec, ...(config.mode !== "off" && plan ? { planConformance: plan } : {}), ...(findings ? { findings } : {}), ...(config.mode !== "off" && definition && playbookStage(definition.base) ? { playbook: playbookStage(definition.base) } : {}), ...(shown ? { criteria } : {}), ...(custom ? { definition, ...custom } : {}), mode: config.mode, stage, blockers, ready: blockers.length === 0,
|
|
274
287
|
actor: !definition ? "person" : shown && criteria.blockers.length ? "reviewer/person" : config.definitions ? definition.actor : baseActor(definition.base),
|
|
275
288
|
next: blockers[0] ?? "person may inspect merge readiness; no automatic merge" };
|
|
276
289
|
}
|
|
@@ -434,13 +447,15 @@ export async function readReviewReadiness(receipt: SdlcReceipt, expected: string
|
|
|
434
447
|
if (thread?.isResolved !== false) { blockers.push("bot review thread evidence unavailable"); continue; }
|
|
435
448
|
const comments = thread?.comments;
|
|
436
449
|
if (comments?.pageInfo?.hasNextPage !== false || !Array.isArray(comments.nodes) || comments.nodes.length > 100
|
|
437
|
-
|| comments.nodes.some((comment: {
|
|
450
|
+
|| comments.nodes.some((comment: { createdAt?: string }) => !Number.isFinite(Date.parse(comment?.createdAt ?? "")))) {
|
|
438
451
|
blockers.push("bot review thread evidence unavailable"); continue;
|
|
439
452
|
}
|
|
453
|
+
// #230 item 3: only a `User` comment answers. Every other or missing author kind (Bot, null, Organization, Mannequin,
|
|
454
|
+
// EnterpriseUserAccount, a kind GitHub adds later) is a pending bot comment, so an unresolved thread settles by
|
|
455
|
+
// resolution or a later User comment. No weaker than before, and still no body or login is read.
|
|
440
456
|
let unanswered = false;
|
|
441
|
-
for (const comment of comments.nodes) {
|
|
442
|
-
|
|
443
|
-
else if (comment.author.__typename === "User") unanswered = false;
|
|
457
|
+
for (const comment of comments.nodes as { author?: { __typename?: string | null } | null }[]) {
|
|
458
|
+
unanswered = comment?.author?.__typename !== "User";
|
|
444
459
|
}
|
|
445
460
|
if (unanswered) blockers.push("bot review threads unanswered");
|
|
446
461
|
}
|
package/src/hub/settings.ts
CHANGED
|
@@ -75,7 +75,7 @@ export const SETTINGS: readonly SettingDef[] = [
|
|
|
75
75
|
* Never editable from settings, whatever the session: these choose what the hub executes or where secrets and task
|
|
76
76
|
* text go. A key equal to an entry, or below it, is refused by name.
|
|
77
77
|
*/
|
|
78
|
-
export const NEVER_EDITABLE = ["kimi_cmd", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "codex_bin", "pi.cmd", "checks", "mlx", "omniroute", "memory.worker_url", "local.read_allow", "local.bash_network", "local.network_allow", "bench.enabled", "terminal", "instances", "adapters"] as const;
|
|
78
|
+
export const NEVER_EDITABLE = ["kimi_cmd", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "codex_bin", "pi.cmd", "checks", "mlx", "omniroute", "memory.worker_url", "local.read_allow", "local.bash_network", "local.network_allow", "bench.enabled", "terminal", "instances", "adapters", "adapter_modules"] as const;
|
|
79
79
|
|
|
80
80
|
/** Derive project-bound rows from the same builtin templates used by lookup, without registering aliases globally. */
|
|
81
81
|
export function projectSettings(instances?: KindMap, adapterCatalog?: ProjectAdapterCatalog): readonly SettingDef[] {
|
|
@@ -384,6 +384,10 @@ export function undoSetting(cwd: string, stateDir: string, validate: (scratch: s
|
|
|
384
384
|
export function storedLocally(cwd: string, def: SettingDef): boolean {
|
|
385
385
|
try { return valueAt(JSON.parse(readFileSync(join(cwd, ".agenthub", CONFIG_LOCAL), "utf8")), def.path) !== undefined; } catch { return false; }
|
|
386
386
|
}
|
|
387
|
+
/** What the shared `.agenthub/config.json` holds for the key, or undefined: a removal here only ever touches the machine-local file. */
|
|
388
|
+
export function sharedValue(cwd: string, def: SettingDef): unknown {
|
|
389
|
+
try { return valueAt(JSON.parse(readFileSync(join(cwd, ".agenthub", "config.json"), "utf8")), def.path); } catch { return undefined; }
|
|
390
|
+
}
|
|
387
391
|
/**
|
|
388
392
|
* For a config setting: the value the config files would hold for it after the pending undo (the previous version of
|
|
389
393
|
* the machine-local file, else the shared file), or, with `inherit`, after the machine-local value is removed (the
|
package/src/hub/snapshots.ts
CHANGED
|
@@ -20,19 +20,21 @@ import { HUB_DB_BUSY_TIMEOUT_MS } from "./db-timeout.ts";
|
|
|
20
20
|
* ponytail: the trees are unreferenced, so `git gc` may prune them after gc.pruneExpire (two weeks by default); undo
|
|
21
21
|
* of an older turn says so. Refs under refs/agenthub/ would keep them, at the cost of showing up in the user's repo.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
/** `now` is a host-only clock (#661): a test holds it fixed so the budget never runs out whatever the runner's speed. Production leaves it unset (performance.now). */
|
|
24
|
+
export interface SnapshotBudget { deadline: number; now?: () => number; }
|
|
25
|
+
export const budgetNow = (budget: SnapshotBudget): number => budget.now ? budget.now() : performance.now();
|
|
24
26
|
export class SnapshotBudgetExceeded extends Error {
|
|
25
27
|
constructor() { super("snapshot burst budget exhausted; boundary evidence is unknown"); }
|
|
26
28
|
}
|
|
27
29
|
const git = (top: string | Repo, args: string[], env: Record<string, string> = {}, budget?: SnapshotBudget, maxBuffer?: number) => {
|
|
28
30
|
const argv = repoGitArgs(top, args);
|
|
29
|
-
const remaining = budget ? budget.deadline -
|
|
31
|
+
const remaining = budget ? budget.deadline - budgetNow(budget) : SNAPSHOT_TIMEOUT_MS;
|
|
30
32
|
if (remaining <= 0) throw new SnapshotBudgetExceeded();
|
|
31
33
|
const result = hubGitSync(argv, {
|
|
32
34
|
encoding: "utf8", env: { ...repoGitEnv(top), ...env }, ...(maxBuffer === undefined ? {} : { maxBuffer }),
|
|
33
35
|
timeout: Math.max(1, Math.min(SNAPSHOT_TIMEOUT_MS, Math.floor(remaining))),
|
|
34
36
|
});
|
|
35
|
-
if (budget && (
|
|
37
|
+
if (budget && (budgetNow(budget) >= budget.deadline || (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT")) throw new SnapshotBudgetExceeded();
|
|
36
38
|
return result;
|
|
37
39
|
};
|
|
38
40
|
const SNAPSHOT_TIMEOUT_MS = 10_000;
|
|
@@ -146,7 +148,7 @@ export function branchHeads(repo: Repo, budget?: SnapshotBudget): string | undef
|
|
|
146
148
|
const result = git(repo, ["for-each-ref", "--sort=refname", "--format=%(refname) %(objectname)", "refs/heads/", "refs/remotes/"], {}, budget, 1024 * 1024);
|
|
147
149
|
if (result.status !== 0 || result.error) return undefined;
|
|
148
150
|
const digest = createHash("sha256").update(result.stdout).digest("hex");
|
|
149
|
-
if (budget &&
|
|
151
|
+
if (budget && budgetNow(budget) >= budget.deadline) throw new SnapshotBudgetExceeded();
|
|
150
152
|
return digest;
|
|
151
153
|
}
|
|
152
154
|
|
|
@@ -158,7 +160,7 @@ export function soleWorktree(repo: Repo, budget?: SnapshotBudget): boolean | und
|
|
|
158
160
|
const registeredRoot = repo.shadow ? repo.dir : repo.top;
|
|
159
161
|
let count = 0;
|
|
160
162
|
for (const field of result.stdout.split("\0")) {
|
|
161
|
-
if (budget &&
|
|
163
|
+
if (budget && budgetNow(budget) >= budget.deadline) throw new SnapshotBudgetExceeded();
|
|
162
164
|
if (!field.startsWith("worktree ")) continue;
|
|
163
165
|
if (field.slice(9) !== registeredRoot || ++count > 1) return false;
|
|
164
166
|
}
|
|
@@ -179,7 +181,7 @@ export interface HeadMoveCursor {
|
|
|
179
181
|
const HEAD_LOG_TAIL_BYTES = 4096;
|
|
180
182
|
const HEAD_LOG_MAX_APPEND_BYTES = 128 * 1024;
|
|
181
183
|
const HEAD_LOG_MAX_MOVES = 64;
|
|
182
|
-
const checkSnapshotBudget = (budget?: SnapshotBudget) => { if (budget &&
|
|
184
|
+
const checkSnapshotBudget = (budget?: SnapshotBudget) => { if (budget && budgetNow(budget) >= budget.deadline) throw new SnapshotBudgetExceeded(); };
|
|
183
185
|
const sameLogFile = (a: Stats, b: Stats) => a.dev === b.dev && a.ino === b.ino;
|
|
184
186
|
function readHeadLog<T>(repo: Repo, budget: SnapshotBudget | undefined, read: (fd: number, stat: Stats) => T | undefined): T | undefined {
|
|
185
187
|
let fd: number | undefined;
|