@geraldmaron/construct 1.0.7 → 1.0.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/README.md +20 -17
- package/bin/construct +1199 -69
- package/commands/work/optimize-prompts.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +2 -2
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +5 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +65 -17
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -0
- package/lib/doctor/watchers/consistency.mjs +62 -12
- package/lib/document-extract.mjs +221 -28
- package/lib/document-ingest.mjs +2 -0
- package/lib/embed/inbox.mjs +48 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/env-config.mjs +9 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +12 -0
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +18 -0
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +222 -55
- package/lib/intake/filesystem-queue.mjs +25 -5
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -0
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/mcp/server.mjs +9 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/migrations/index.mjs +1 -1
- package/lib/model-registry.mjs +1 -1
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +7 -1
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +13 -3
- package/personas/construct.md +2 -2
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/no-fabrication.md +1 -1
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +126 -54
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/{agents → specialists}/contracts.schema.json +1 -1
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +1 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-architect.md +1 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +1 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +1 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-debugger.md +1 -1
- package/{agents → specialists}/prompts/cx-designer.md +1 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +1 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +2 -2
- package/{agents → specialists}/prompts/cx-engineer.md +2 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +1 -1
- package/{agents → specialists}/prompts/cx-explorer.md +1 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +1 -1
- package/{agents → specialists}/prompts/cx-operations.md +1 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +2 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +1 -1
- package/{agents → specialists}/prompts/cx-qa.md +1 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +1 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +1 -1
- package/{agents → specialists}/prompts/cx-researcher.md +1 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +2 -2
- package/{agents → specialists}/prompts/cx-security.md +1 -1
- package/{agents → specialists}/prompts/cx-sre.md +2 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +1 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +3 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +1 -1
- package/{agents → specialists}/registry.json +651 -423
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- /package/{agents → specialists}/contracts.json +0 -0
- /package/{agents → specialists}/teams.json +0 -0
|
@@ -38,11 +38,11 @@ export const SUPPORTED_CATEGORIES = Object.freeze([
|
|
|
38
38
|
|
|
39
39
|
const CATEGORY_RULES = {
|
|
40
40
|
personas: { originalDir: 'personas', defaultExt: '.md' },
|
|
41
|
-
agents: { originalDir: '
|
|
41
|
+
agents: { originalDir: 'specialists/prompts', defaultExt: '.md' },
|
|
42
42
|
skills: { originalDir: 'skills', defaultExt: '.md' },
|
|
43
43
|
rules: { originalDir: 'rules', defaultExt: '.md' },
|
|
44
|
-
contracts: { originalDir: '
|
|
45
|
-
'role-manifests': { originalDir: '
|
|
44
|
+
contracts: { originalDir: 'specialists', defaultExt: '.json', singleFile: 'contracts.json' },
|
|
45
|
+
'role-manifests': { originalDir: 'specialists', defaultExt: '.json', singleFile: 'role-manifests.json' },
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function categoryRule(category) {
|
package/lib/parity.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* After `construct sync` writes adapters to multiple surfaces (Claude Code,
|
|
5
5
|
* OpenCode, Codex, Copilot, VS Code, Cursor), the module diffs each surface's actual state against the
|
|
6
|
-
* canonical `
|
|
6
|
+
* canonical `specialists/registry.json`. Backs `construct doctor` for surfacing
|
|
7
7
|
* silent divergence — for instance, an agent added to the registry that
|
|
8
8
|
* never made it to OpenCode because of a sync regression.
|
|
9
9
|
*
|
|
@@ -31,7 +31,7 @@ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
31
31
|
const ROOT_DIR = path.resolve(MODULE_DIR, '..');
|
|
32
32
|
|
|
33
33
|
function loadRegistry(rootDir = ROOT_DIR) {
|
|
34
|
-
const file = path.join(rootDir, '
|
|
34
|
+
const file = path.join(rootDir, 'specialists', 'registry.json');
|
|
35
35
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -42,8 +42,8 @@ function adapterName(entry, prefix) {
|
|
|
42
42
|
function entriesForSurface(registry, surface) {
|
|
43
43
|
const prefix = registry.prefix || 'cx';
|
|
44
44
|
const entries = [
|
|
45
|
-
...(registry.
|
|
46
|
-
...(registry.
|
|
45
|
+
...(registry.orchestrator ? [{ ...registry.orchestrator, isPersona: true }] : []),
|
|
46
|
+
...(registry.specialists || []).map((s) => ({ ...s, isPersona: false })),
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
return entries
|
package/lib/policy/engine.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/policy/engine.mjs — role-based policy decisions for tool / action access.
|
|
3
3
|
*
|
|
4
|
-
* Reads role permissions from `
|
|
4
|
+
* Reads role permissions from `specialists/role-manifests.json` and decides
|
|
5
5
|
* whether a given (role, project, tool, action, risk) tuple is allowed,
|
|
6
6
|
* needs approval, or is denied outright. Powers the MCP broker for team
|
|
7
7
|
* and enterprise deployments; solo mode leaves the broker off so this
|
|
@@ -20,7 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
|
|
22
22
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const DEFAULT_MANIFEST_PATH = path.join(MODULE_DIR, '..', '..', '
|
|
23
|
+
const DEFAULT_MANIFEST_PATH = path.join(MODULE_DIR, '..', '..', 'specialists', 'role-manifests.json');
|
|
24
24
|
|
|
25
25
|
const HIGH_RISK_AUTONOMOUS = new Set(['security', 'sre', 'release-manager']);
|
|
26
26
|
|
|
@@ -76,7 +76,7 @@ Goal: define the role set and how it interlocks with the curated cx-* registry.
|
|
|
76
76
|
reuse-existing (name the cx-* agent), create-new (specify scope), or compose-overlay (which base role + which flavor).
|
|
77
77
|
- Identify cross-role handoffs. Where can ambiguity stall the loop? Name the orchestrator role.
|
|
78
78
|
- Validate against the per-role cap (max 6 flavors per role per profile).
|
|
79
|
-
- Acceptance: every declared role either exists in
|
|
79
|
+
- Acceptance: every declared role either exists in specialists/registry.json or has a written scope statement.
|
|
80
80
|
|
|
81
81
|
## 4. Validation (cx-evaluator)
|
|
82
82
|
|
package/lib/prompt-metadata.mjs
CHANGED
|
@@ -25,9 +25,9 @@ function normalizeName(name) {
|
|
|
25
25
|
function findEntry(registry, agentName) {
|
|
26
26
|
const normalized = normalizeName(agentName);
|
|
27
27
|
const entries = [
|
|
28
|
-
|
|
29
|
-
...(registry.
|
|
30
|
-
];
|
|
28
|
+
registry.orchestrator,
|
|
29
|
+
...(registry.specialists ?? []),
|
|
30
|
+
].filter(Boolean);
|
|
31
31
|
return entries.find((entry) => {
|
|
32
32
|
const entryName = normalizeName(entry.name);
|
|
33
33
|
return entryName === normalized || `cx-${entryName}` === String(agentName ?? '').trim().toLowerCase();
|
|
@@ -36,7 +36,7 @@ function findEntry(registry, agentName) {
|
|
|
36
36
|
|
|
37
37
|
export function resolvePromptEntry(agentName, { rootDir = process.cwd(), registry } = {}) {
|
|
38
38
|
if (!agentName) return null;
|
|
39
|
-
const loadedRegistry = registry ?? readJson(path.join(rootDir, '
|
|
39
|
+
const loadedRegistry = registry ?? readJson(path.join(rootDir, 'specialists', 'registry.json'));
|
|
40
40
|
if (!loadedRegistry) return null;
|
|
41
41
|
return findEntry(loadedRegistry, agentName);
|
|
42
42
|
}
|
|
@@ -104,6 +104,20 @@ export function getBreaker(key, options) {
|
|
|
104
104
|
return REGISTRY.get(key);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
export function getState(providerName) {
|
|
108
|
+
const b = REGISTRY.get(providerName);
|
|
109
|
+
if (!b) return null;
|
|
110
|
+
return {
|
|
111
|
+
state: b.state.toUpperCase(),
|
|
112
|
+
failures: b.failureCount,
|
|
113
|
+
lastFailure: b.openedAt ? new Date(b.openedAt) : null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function reset(providerName) {
|
|
118
|
+
REGISTRY.get(providerName)?.reset();
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
export function describeBreakers() {
|
|
108
122
|
return [...REGISTRY.values()].map((b) => ({
|
|
109
123
|
key: b.key,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A provider integrates Construct with an external system that holds work
|
|
5
5
|
* artefacts: GitHub repos, Jira projects, Confluence spaces, Slack channels,
|
|
6
|
-
* Salesforce orgs, etc. Providers are stateless adapters
|
|
6
|
+
* Salesforce orgs, etc. Providers are stateless adapters; durable state
|
|
7
7
|
* (cached search results, last-seen markers, observations distilled from
|
|
8
8
|
* fetched items) lives in core stores, not in the provider.
|
|
9
9
|
*
|
|
@@ -29,11 +29,75 @@
|
|
|
29
29
|
*
|
|
30
30
|
* `assertProviderContract(provider)` runs at registry load time and refuses
|
|
31
31
|
* to accept providers that miss required fields or declare capabilities
|
|
32
|
-
* they don't implement. The check is deliberately loose
|
|
33
|
-
* extend the shape with extra methods
|
|
32
|
+
* they don't implement. The check is deliberately loose; providers can
|
|
33
|
+
* extend the shape with extra methods; but it gates the load-bearing
|
|
34
34
|
* fields so a typo in a plugin can't take down the registry.
|
|
35
|
+
*
|
|
36
|
+
* Scope validation: `ALLOWLIST_SCHEMA` declares the allowlist fields
|
|
37
|
+
* supported per provider. `validateAllowlist(providerName, target, config)`
|
|
38
|
+
* checks whether `target` (a repo name, channel ID, project key, etc.) is
|
|
39
|
+
* permitted by the caller's config. When no allowlist fields are set the
|
|
40
|
+
* call is allowed through; when any allowlist field is set the target must
|
|
41
|
+
* satisfy it.
|
|
42
|
+
*
|
|
43
|
+
* Glob matching for `*Glob` fields follows minimatch-style single-segment
|
|
44
|
+
* patterns where `*` matches any sequence of non-slash characters.
|
|
35
45
|
*/
|
|
36
46
|
|
|
47
|
+
export const ALLOWLIST_SCHEMA = Object.freeze({
|
|
48
|
+
github: { fields: ['org', 'repoAllowlist', 'repoAllowGlob'] },
|
|
49
|
+
'atlassian-jira': { fields: ['instance', 'projectAllowlist'] },
|
|
50
|
+
'atlassian-confluence': { fields: ['instance', 'spaceAllowlist'] },
|
|
51
|
+
slack: { fields: ['workspace', 'channelAllowlist'] },
|
|
52
|
+
salesforce: { fields: ['instance', 'objectAllowlist'] },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function globMatch(pattern, value) {
|
|
56
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
|
|
57
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateAllowlist(providerName, target, config = {}) {
|
|
61
|
+
const schema = ALLOWLIST_SCHEMA[providerName];
|
|
62
|
+
|
|
63
|
+
// Unknown providers pass through; no schema means no restriction.
|
|
64
|
+
if (!schema) return { allowed: true, reason: 'no allowlist schema for provider' };
|
|
65
|
+
|
|
66
|
+
const listFields = schema.fields.filter((f) => f.endsWith('Allowlist'));
|
|
67
|
+
const globFields = schema.fields.filter((f) => f.endsWith('AllowGlob'));
|
|
68
|
+
|
|
69
|
+
const hasListConstraint = listFields.some((f) => Array.isArray(config[f]) && config[f].length > 0);
|
|
70
|
+
const hasGlobConstraint = globFields.some((f) => typeof config[f] === 'string' && config[f].length > 0);
|
|
71
|
+
|
|
72
|
+
// No allowlist configured: permit everything.
|
|
73
|
+
if (!hasListConstraint && !hasGlobConstraint) {
|
|
74
|
+
return { allowed: true, reason: 'no allowlist configured' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check explicit list membership.
|
|
78
|
+
for (const field of listFields) {
|
|
79
|
+
if (Array.isArray(config[field]) && config[field].length > 0) {
|
|
80
|
+
if (config[field].includes(target)) {
|
|
81
|
+
return { allowed: true, reason: `matched ${field}` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check glob patterns.
|
|
87
|
+
for (const field of globFields) {
|
|
88
|
+
if (typeof config[field] === 'string' && config[field].length > 0) {
|
|
89
|
+
if (globMatch(config[field], target)) {
|
|
90
|
+
return { allowed: true, reason: `matched ${field} pattern '${config[field]}'` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
allowed: false,
|
|
97
|
+
reason: `'${target}' is not in the configured allowlist for ${providerName}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
37
101
|
export const CAPABILITIES = Object.freeze([
|
|
38
102
|
'read',
|
|
39
103
|
'search',
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/creds.mjs — credential management for provider integrations.
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes provider credentials stored in `~/.construct/config.env`.
|
|
5
|
+
* Each provider block is delimited by comment markers so the file can be
|
|
6
|
+
* parsed without a full ini/dotenv library and can also be sourced by a shell.
|
|
7
|
+
*
|
|
8
|
+
* Block format (example for `github`):
|
|
9
|
+
*
|
|
10
|
+
* # CONSTRUCT_CREDS_GITHUB
|
|
11
|
+
* CONSTRUCT_CREDS_GITHUB_KEY=ghp_xxx
|
|
12
|
+
* CONSTRUCT_CREDS_GITHUB_ACCOUNT=myorg
|
|
13
|
+
* CONSTRUCT_CREDS_GITHUB_ROTATED_AT=2026-05-27
|
|
14
|
+
* # END_CONSTRUCT_CREDS_GITHUB
|
|
15
|
+
*
|
|
16
|
+
* Security: `writeCreds` sets the file mode to 0600 after every write.
|
|
17
|
+
* `checkCredsFileMode` reports whether the current mode is within policy.
|
|
18
|
+
*
|
|
19
|
+
* All functions are synchronous except those that need the filesystem and
|
|
20
|
+
* are documented as async. Currently all ops are sync; the async signature
|
|
21
|
+
* is reserved for future keychain-backend support.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
|
|
28
|
+
const CONSTRUCT_DIR = path.join(os.homedir(), '.construct');
|
|
29
|
+
const CONFIG_FILE = 'config.env';
|
|
30
|
+
|
|
31
|
+
export function credsFilePath() {
|
|
32
|
+
return path.join(CONSTRUCT_DIR, CONFIG_FILE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function providerKey(provider) {
|
|
36
|
+
return provider.toUpperCase().replace(/-/g, '_');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function blockStart(key) {
|
|
40
|
+
return `# CONSTRUCT_CREDS_${key}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function blockEnd(key) {
|
|
44
|
+
return `# END_CONSTRUCT_CREDS_${key}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readRaw() {
|
|
48
|
+
const fp = credsFilePath();
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(fp, 'utf8');
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code === 'ENOENT') return '';
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeRaw(content) {
|
|
58
|
+
const fp = credsFilePath();
|
|
59
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
60
|
+
fs.writeFileSync(fp, content, { encoding: 'utf8', mode: 0o600 });
|
|
61
|
+
|
|
62
|
+
// Re-apply mode after write; some platforms reset on open.
|
|
63
|
+
fs.chmodSync(fp, 0o600);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractBlocks(raw) {
|
|
67
|
+
const blocks = {};
|
|
68
|
+
const lines = raw.split('\n');
|
|
69
|
+
let current = null;
|
|
70
|
+
let currentLines = [];
|
|
71
|
+
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
const startMatch = line.match(/^# CONSTRUCT_CREDS_([A-Z0-9_]+)$/);
|
|
74
|
+
const endMatch = line.match(/^# END_CONSTRUCT_CREDS_([A-Z0-9_]+)$/);
|
|
75
|
+
|
|
76
|
+
if (startMatch) {
|
|
77
|
+
current = startMatch[1];
|
|
78
|
+
currentLines = [line];
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (endMatch && current === endMatch[1]) {
|
|
82
|
+
currentLines.push(line);
|
|
83
|
+
blocks[current] = currentLines.join('\n');
|
|
84
|
+
current = null;
|
|
85
|
+
currentLines = [];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (current) {
|
|
89
|
+
currentLines.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return blocks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseBlock(blockText) {
|
|
97
|
+
const result = {};
|
|
98
|
+
for (const line of blockText.split('\n')) {
|
|
99
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
100
|
+
if (!m) continue;
|
|
101
|
+
result[m[1]] = m[2];
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildBlock(key, { account, apiKey, rotatedAt }) {
|
|
107
|
+
const prefix = `CONSTRUCT_CREDS_${key}`;
|
|
108
|
+
const lines = [blockStart(key)];
|
|
109
|
+
if (apiKey !== undefined) lines.push(`${prefix}_KEY=${apiKey}`);
|
|
110
|
+
if (account !== undefined) lines.push(`${prefix}_ACCOUNT=${account}`);
|
|
111
|
+
if (rotatedAt !== undefined) lines.push(`${prefix}_ROTATED_AT=${rotatedAt}`);
|
|
112
|
+
lines.push(blockEnd(key));
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function nextRotationDue(rotatedAtStr) {
|
|
117
|
+
if (!rotatedAtStr) return null;
|
|
118
|
+
const d = new Date(rotatedAtStr);
|
|
119
|
+
if (isNaN(d.getTime())) return null;
|
|
120
|
+
d.setDate(d.getDate() + 90);
|
|
121
|
+
return d.toISOString().slice(0, 10);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function readCreds() {
|
|
125
|
+
const raw = readRaw();
|
|
126
|
+
const blocks = extractBlocks(raw);
|
|
127
|
+
const result = {};
|
|
128
|
+
|
|
129
|
+
for (const [upperKey, blockText] of Object.entries(blocks)) {
|
|
130
|
+
const prefix = `CONSTRUCT_CREDS_${upperKey}`;
|
|
131
|
+
const fields = parseBlock(blockText);
|
|
132
|
+
const rotatedAt = fields[`${prefix}_ROTATED_AT`] || null;
|
|
133
|
+
result[upperKey.toLowerCase().replace(/_/g, '-')] = {
|
|
134
|
+
account: fields[`${prefix}_ACCOUNT`] || null,
|
|
135
|
+
key: fields[`${prefix}_KEY`] || null,
|
|
136
|
+
rotatedAt,
|
|
137
|
+
nextRotationDue: nextRotationDue(rotatedAt),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function writeCreds(provider, { key, account } = {}) {
|
|
145
|
+
const pKey = providerKey(provider);
|
|
146
|
+
const raw = readRaw();
|
|
147
|
+
const blocks = extractBlocks(raw);
|
|
148
|
+
|
|
149
|
+
const existing = blocks[pKey] ? parseBlock(blocks[pKey]) : {};
|
|
150
|
+
const prefix = `CONSTRUCT_CREDS_${pKey}`;
|
|
151
|
+
|
|
152
|
+
const rotatedAt = key !== undefined
|
|
153
|
+
? new Date().toISOString().slice(0, 10)
|
|
154
|
+
: (existing[`${prefix}_ROTATED_AT`] || null);
|
|
155
|
+
|
|
156
|
+
const mergedAccount = account !== undefined ? account : (existing[`${prefix}_ACCOUNT`] || undefined);
|
|
157
|
+
const mergedKey = key !== undefined ? key : (existing[`${prefix}_KEY`] || undefined);
|
|
158
|
+
|
|
159
|
+
const newBlock = buildBlock(pKey, { account: mergedAccount, apiKey: mergedKey, rotatedAt });
|
|
160
|
+
|
|
161
|
+
let newRaw;
|
|
162
|
+
if (blocks[pKey]) {
|
|
163
|
+
const startIdx = raw.indexOf(blockStart(pKey));
|
|
164
|
+
const endIdx = raw.indexOf(blockEnd(pKey)) + blockEnd(pKey).length;
|
|
165
|
+
newRaw = raw.slice(0, startIdx) + newBlock + raw.slice(endIdx);
|
|
166
|
+
} else {
|
|
167
|
+
const trimmed = raw.trimEnd();
|
|
168
|
+
newRaw = trimmed ? `${trimmed}\n\n${newBlock}\n` : `${newBlock}\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeRaw(newRaw);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function deleteCreds(provider) {
|
|
175
|
+
const pKey = providerKey(provider);
|
|
176
|
+
const raw = readRaw();
|
|
177
|
+
const start = blockStart(pKey);
|
|
178
|
+
const end = blockEnd(pKey);
|
|
179
|
+
|
|
180
|
+
const startIdx = raw.indexOf(start);
|
|
181
|
+
if (startIdx === -1) return;
|
|
182
|
+
|
|
183
|
+
const endIdx = raw.indexOf(end, startIdx);
|
|
184
|
+
if (endIdx === -1) return;
|
|
185
|
+
|
|
186
|
+
const before = raw.slice(0, startIdx).trimEnd();
|
|
187
|
+
const after = raw.slice(endIdx + end.length).trimStart();
|
|
188
|
+
|
|
189
|
+
const newRaw = before && after
|
|
190
|
+
? `${before}\n\n${after}`
|
|
191
|
+
: before
|
|
192
|
+
? `${before}\n`
|
|
193
|
+
: after || '';
|
|
194
|
+
|
|
195
|
+
writeRaw(newRaw);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function checkCredsFileMode() {
|
|
199
|
+
const fp = credsFilePath();
|
|
200
|
+
try {
|
|
201
|
+
const stat = fs.statSync(fp);
|
|
202
|
+
const mode = stat.mode & 0o777;
|
|
203
|
+
const modeStr = '0' + mode.toString(8);
|
|
204
|
+
return { ok: mode <= 0o600, mode: modeStr };
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err.code === 'ENOENT') return { ok: true, mode: 'absent' };
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function listCreds() {
|
|
212
|
+
const all = readCreds();
|
|
213
|
+
return Object.entries(all).map(([provider, data]) => ({
|
|
214
|
+
provider,
|
|
215
|
+
account: data.account,
|
|
216
|
+
rotatedAt: data.rotatedAt,
|
|
217
|
+
nextRotationDue: data.nextRotationDue,
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/scaffold.mjs — provider scaffold generator.
|
|
3
|
+
*
|
|
4
|
+
* Reads the template files from `templates/provider-scaffold/`, substitutes
|
|
5
|
+
* the %%PROVIDER_NAME%% and %%CAPABILITIES%% placeholders, and writes the
|
|
6
|
+
* result into `lib/providers/<name>/`. Returns the list of files created so
|
|
7
|
+
* the CLI can report them to the user.
|
|
8
|
+
*
|
|
9
|
+
* Supported capabilities: read, search, write, watch, webhook.
|
|
10
|
+
* The `read` and `search` methods are always included in the generated
|
|
11
|
+
* index.mjs; additional capabilities listed in `options.capabilities` add
|
|
12
|
+
* stub implementations for write, watch, and webhook.
|
|
13
|
+
*
|
|
14
|
+
* `scaffoldProvider` is idempotent with respect to the directory: it will
|
|
15
|
+
* not overwrite an existing file unless `options.force = true`. This prevents
|
|
16
|
+
* accidental loss of work when a developer reruns the command.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', '..', 'templates', 'provider-scaffold');
|
|
25
|
+
|
|
26
|
+
const ALL_CAPABILITIES = ['read', 'search', 'write', 'watch', 'webhook'];
|
|
27
|
+
|
|
28
|
+
function capabilityStub(cap) {
|
|
29
|
+
const stubs = {
|
|
30
|
+
write: `
|
|
31
|
+
async write(payload, config = {}) {
|
|
32
|
+
// Replace with a real write operation against the remote system. // construct-lint-ignore
|
|
33
|
+
throw new Error('not implemented');
|
|
34
|
+
},`,
|
|
35
|
+
watch: `
|
|
36
|
+
async watch(config = {}, callback) {
|
|
37
|
+
// Replace with a real subscription; call callback(item) on new events. // construct-lint-ignore
|
|
38
|
+
throw new Error('not implemented');
|
|
39
|
+
},`,
|
|
40
|
+
webhook: `
|
|
41
|
+
async webhook(config = {}, request) {
|
|
42
|
+
// Replace with real webhook signature verification and dispatch. // construct-lint-ignore
|
|
43
|
+
return { ok: false, error: 'not implemented' };
|
|
44
|
+
},`,
|
|
45
|
+
};
|
|
46
|
+
return stubs[cap] || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderIndex(name, capabilities) {
|
|
50
|
+
const capList = capabilities.filter((c) => ALL_CAPABILITIES.includes(c));
|
|
51
|
+
const capString = capList.join(', ');
|
|
52
|
+
|
|
53
|
+
const extraStubs = capList
|
|
54
|
+
.filter((c) => !['read', 'search'].includes(c))
|
|
55
|
+
.map(capabilityStub)
|
|
56
|
+
.join('\n');
|
|
57
|
+
|
|
58
|
+
const template = fs.readFileSync(path.join(TEMPLATES_DIR, 'index.mjs'), 'utf8');
|
|
59
|
+
let rendered = template
|
|
60
|
+
.replace(/%%PROVIDER_NAME%%/g, name)
|
|
61
|
+
.replace(/%%CAPABILITIES%%/g, capString);
|
|
62
|
+
|
|
63
|
+
if (extraStubs) {
|
|
64
|
+
rendered = rendered.replace(
|
|
65
|
+
/(\s+async search\(query, config = \{\}\) \{[\s\S]*?throw new Error\('not implemented'\);\n\s+\},)/,
|
|
66
|
+
`$1\n${extraStubs}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return rendered;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderTest(name) {
|
|
74
|
+
const template = fs.readFileSync(path.join(TEMPLATES_DIR, 'health.test.mjs'), 'utf8');
|
|
75
|
+
return template.replace(/%%PROVIDER_NAME%%/g, name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function scaffoldProvider(name, options = {}) {
|
|
79
|
+
const {
|
|
80
|
+
capabilities = ['read', 'search'],
|
|
81
|
+
cwd = process.cwd(),
|
|
82
|
+
force = false,
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
if (!name || typeof name !== 'string' || !/^[a-z0-9-]+$/.test(name)) {
|
|
86
|
+
throw new Error(`scaffoldProvider: name must be a lowercase alphanumeric-and-hyphen string, got '${name}'`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const providerDir = path.join(cwd, 'lib', 'providers', name);
|
|
90
|
+
fs.mkdirSync(providerDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
const files = [];
|
|
93
|
+
|
|
94
|
+
const indexPath = path.join(providerDir, 'index.mjs');
|
|
95
|
+
if (force || !fs.existsSync(indexPath)) {
|
|
96
|
+
fs.writeFileSync(indexPath, renderIndex(name, capabilities), 'utf8');
|
|
97
|
+
files.push(indexPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const testPath = path.join(providerDir, 'health.test.mjs');
|
|
101
|
+
if (force || !fs.existsSync(testPath)) {
|
|
102
|
+
fs.writeFileSync(testPath, renderTest(name), 'utf8');
|
|
103
|
+
files.push(testPath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { providerDir, files };
|
|
107
|
+
}
|
package/lib/role-preload.mjs
CHANGED
|
@@ -34,11 +34,16 @@ function stripTopHeading(body) {
|
|
|
34
34
|
export function readRoleFile(root, name, opts = {}) {
|
|
35
35
|
const p = path.join(root, "skills", "roles", `${name}.md`);
|
|
36
36
|
if (!fs.existsSync(p)) return null;
|
|
37
|
+
const t0 = Date.now();
|
|
37
38
|
const body = stripTopHeading(stripFrontmatter(fs.readFileSync(p, "utf8"))).trim();
|
|
38
39
|
logSkillCall({
|
|
39
40
|
skillId: `roles/${name}`,
|
|
40
41
|
source: opts.source || 'role-preload',
|
|
41
42
|
callerContext: opts.callerContext,
|
|
43
|
+
latencyMs: Date.now() - t0,
|
|
44
|
+
agentId: opts.agentId,
|
|
45
|
+
sessionId: opts.sessionId,
|
|
46
|
+
tokensReturned: Math.ceil(body.length / 4),
|
|
42
47
|
});
|
|
43
48
|
return body;
|
|
44
49
|
}
|
package/lib/roles/catalog.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/roles/catalog.mjs — role listing and display helpers.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
4
|
+
* Reads specialists/registry.json to produce a list of available roles with
|
|
5
5
|
* optional department grouping and consolidated-role views. Consumed by the
|
|
6
6
|
* `roles:list` CLI command.
|
|
7
7
|
*/
|
package/lib/roles/manifest.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/roles/manifest.mjs — role manifest loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
4
|
+
* Reads specialists/role-manifests.json. A persona is "onboarded" when its
|
|
5
5
|
* `events` array is non-empty. Empty entries reserve the slot for future
|
|
6
6
|
* wiring without code change.
|
|
7
7
|
*/
|
|
@@ -11,7 +11,7 @@ import { dirname, join } from 'node:path';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const MANIFEST_PATH = join(__dirname, '..', '..', '
|
|
14
|
+
const MANIFEST_PATH = join(__dirname, '..', '..', 'specialists', 'role-manifests.json');
|
|
15
15
|
|
|
16
16
|
let cached = null;
|
|
17
17
|
|