Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ build/
/gsd-core/bin/lib/validate-command-router.cjs
/gsd-core/bin/lib/workstream-inventory.cjs
/gsd-core/bin/lib/roadmap-command-router.cjs
/gsd-core/bin/lib/hub-query.cjs
/gsd-core/bin/lib/hub-command-router.cjs
/gsd-core/bin/lib/state-command-router.cjs
/gsd-core/bin/lib/config.cjs
/gsd-core/bin/lib/profile-output.cjs
Expand Down
35 changes: 35 additions & 0 deletions capabilities/hub-mode/capability.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"id": "hub-mode",
"role": "feature",
"title": "Portfolio hub mode",
"description": "Marks a repo as a portfolio hub (multi-milestone numbering, archived phases, cross-repo STATE refs, .gsdrootallow whitelist) and relaxes `validate health` (W002/W005/W006/W007/W019) accordingly. Adds read-only `hub v1 *` query verbs (milestone/phase/phases/manifest/spoke). Only repo_type==='hub' triggers relaxations; 'spoke' is reserved and behaves as 'standalone'.",
"tier": "full",
"requires": [],
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
"skills": [],
"agents": [],
"hooks": [],
"config": {
"repo_type": {
"type": "enum",
"values": ["standalone", "spoke", "hub"],
"default": "standalone",
"description": "Repo topology role. 'hub' relaxes phase-dir/roadmap/STATE/root-file validation for portfolio orchestration repos; 'spoke' is reserved (behaves as standalone today); 'standalone' is the strict default."
},
"workflow.hub_mode": {
"type": "boolean",
"default": false,
"description": "Master toggle for hub-mode behaviors. Reserved on/off gate; the active relaxation branch keys off repo_type==='hub' read directly in validate health."
}
},
"commands": [
{
"family": "hub",
"module": "hub-command-router.cjs",
"router": "routeHubCommand"
}
],
"steps": [],
"contributions": [],
"gates": []
}
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ export default tseslint.config(
'gsd-core/bin/lib/validate-command-router.cjs',
'gsd-core/bin/lib/workstream-inventory.cjs',
'gsd-core/bin/lib/roadmap-command-router.cjs',
'gsd-core/bin/lib/hub-query.cjs',
'gsd-core/bin/lib/hub-command-router.cjs',
'gsd-core/bin/lib/state-command-router.cjs',
'gsd-core/bin/lib/gap-checker.cjs',
'gsd-core/bin/lib/config.cjs',
Expand Down
69 changes: 69 additions & 0 deletions gsd-core/bin/lib/capability-registry.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,50 @@ const capabilities = {
"extendedHookEvents": []
}
},
"hub-mode": {
"id": "hub-mode",
"role": "feature",
"title": "Portfolio hub mode",
"description": "Marks a repo as a portfolio hub (multi-milestone numbering, archived phases, cross-repo STATE refs, .gsdrootallow whitelist) and relaxes `validate health` (W002/W005/W006/W007/W019) accordingly. Adds read-only `hub v1 *` query verbs (milestone/phase/phases/manifest/spoke). Only repo_type==='hub' triggers relaxations; 'spoke' is reserved and behaves as 'standalone'.",
"tier": "full",
"requires": [],
"runtimeCompat": {
"supported": [
"*"
],
"unsupported": []
},
"skills": [],
"agents": [],
"hooks": [],
"config": {
"repo_type": {
"type": "enum",
"values": [
"standalone",
"spoke",
"hub"
],
"default": "standalone",
"description": "Repo topology role. 'hub' relaxes phase-dir/roadmap/STATE/root-file validation for portfolio orchestration repos; 'spoke' is reserved (behaves as standalone today); 'standalone' is the strict default."
},
"workflow.hub_mode": {
"type": "boolean",
"default": false,
"description": "Master toggle for hub-mode behaviors. Reserved on/off gate; the active relaxation branch keys off repo_type==='hub' read directly in validate health."
}
},
"commands": [
{
"family": "hub",
"module": "hub-command-router.cjs",
"router": "routeHubCommand"
}
],
"steps": [],
"contributions": [],
"gates": []
},
"intel": {
"id": "intel",
"role": "feature",
Expand Down Expand Up @@ -2353,6 +2397,8 @@ const configKeys = {
"workflow.schema_drift_gate": "drift",
"workflow.post_planning_gaps": "gap-analysis",
"graphify.enabled": "graphify",
"repo_type": "hub-mode",
"workflow.hub_mode": "hub-mode",
"intel.enabled": "intel",
"mempalace.enabled": "mempalace",
"mempalace.memory_mode": "mempalace",
Expand Down Expand Up @@ -2436,6 +2482,23 @@ const configSchema = {
"default": false,
"description": "Enable the graphify knowledge-graph command + skill."
},
"repo_type": {
"owner": "hub-mode",
"type": "enum",
"default": "standalone",
"description": "Repo topology role. 'hub' relaxes phase-dir/roadmap/STATE/root-file validation for portfolio orchestration repos; 'spoke' is reserved (behaves as standalone today); 'standalone' is the strict default.",
"values": [
"standalone",
"spoke",
"hub"
]
},
"workflow.hub_mode": {
"owner": "hub-mode",
"type": "boolean",
"default": false,
"description": "Master toggle for hub-mode behaviors. Reserved on/off gate; the active relaxation branch keys off repo_type==='hub' read directly in validate health."
},
"intel.enabled": {
"owner": "intel",
"type": "boolean",
Expand Down Expand Up @@ -3517,6 +3580,11 @@ const commandFamilies = {
"module": "graphify-command-router.cjs",
"router": "routeGraphifyCommand"
},
"hub": {
"capId": "hub-mode",
"module": "hub-command-router.cjs",
"router": "routeHubCommand"
},
"intel": {
"capId": "intel",
"module": "intel-command-router.cjs",
Expand Down Expand Up @@ -3641,6 +3709,7 @@ const _requiresGraph = {
"gemini": [],
"graphify": [],
"hermes": [],
"hub-mode": [],
"intel": [],
"kilo": [],
"kimi": [],
Expand Down
87 changes: 87 additions & 0 deletions src/hub-command-router.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Hub command family router — read-only `hub v1 *` query verbs.
*
* Routed via the capability registry's commandFamilies index (ADR-959):
* capabilities/hub-mode/capability.json declares {family:"hub",
* module:"hub-command-router.cjs", router:"routeHubCommand"}, and
* gsd-tools.cjs dispatchCapabilityCommand auto-routes `hub …` here.
*
* SYNCHRONOUS by contract: dispatchCapabilityCommand errors if a router returns
* a Promise (async capability routers are unsupported). All five verbs are
* read-only (mutation:false) — no lock, no writes.
*
* Versioning: requires `hub v1 …`. Unknown version tokens are rejected with an
* InvalidArgs-style error to reserve a future `v2` namespace.
*
* Invocation layout (dispatchCapabilityCommand passes the whole argv tail):
* args[0] = 'hub', args[1] = 'v1', args[2] = verb, args[3+] = verb args.
*
* Modeled on src/roadmap-command-router.cts.
*/

// eslint-disable-next-line @typescript-eslint/no-require-imports -- hub-query.cjs is an export= CommonJS module
import hubQuery = require('./hub-query.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
import ioMod = require('./io.cjs');
const { output } = ioMod;
import { parseNamedArgs } from './command-arg-projection.cjs';

interface RouteHubCommandOptions {
args: string[];
cwd: string;
raw: boolean;
error: (message: string) => void;
}

const HUB_VERBS = ['milestone', 'phase', 'phases', 'manifest', 'spoke'];

function routeHubCommand({ args, cwd, raw, error }: RouteHubCommandOptions): void {
const version = args[1];
if (version !== 'v1') {
error(`hub: only v1 API supported, got "${version ?? '(none)'}"`);
return;
}

const verb = args[2];
switch (verb) {
case 'milestone': {
const sub = args[3];
if (sub !== 'current') {
error(`hub v1 milestone: only 'current' is supported, got "${sub ?? '(none)'}"`);
return;
}
output(hubQuery.getCurrentMilestone(cwd), raw);
return;
}
case 'phase': {
output(hubQuery.getPhase(cwd, args[3]), raw);
return;
}
case 'phases': {
const named = parseNamedArgs(args, ['milestone', 'state']);
output(hubQuery.getPhases(cwd, {
milestone: (named['milestone'] as string | null) ?? undefined,
state: (named['state'] as string | null) ?? undefined,
}), raw);
return;
}
case 'manifest': {
const named = parseNamedArgs(args, ['tier', 'role']);
output(hubQuery.getManifest(cwd, {
tier: (named['tier'] as string | null) ?? undefined,
role: (named['role'] as string | null) ?? undefined,
}), raw);
return;
}
case 'spoke': {
output(hubQuery.getSpoke(cwd, args[3]), raw);
return;
}
default:
error(`Unknown hub verb "${verb ?? '(none)'}". Available: ${HUB_VERBS.join(', ')}`);
}
}

export = {
routeHubCommand,
};
Loading