@geraldmaron/construct 1.0.16 → 1.0.17
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 +4 -1
- package/bin/construct +236 -21
- package/bin/construct-postinstall.mjs +17 -2
- package/lib/cli-commands.mjs +65 -13
- package/lib/embedded-contract/audit.mjs +52 -0
- package/lib/embedded-contract/capability.mjs +179 -0
- package/lib/embedded-contract/contract-version.mjs +39 -0
- package/lib/embedded-contract/envelope.mjs +70 -0
- package/lib/embedded-contract/index.mjs +71 -0
- package/lib/embedded-contract/ingest.mjs +77 -0
- package/lib/embedded-contract/model-resolve.mjs +186 -0
- package/lib/embedded-contract/redaction.mjs +91 -0
- package/lib/embedded-contract/role-facts.mjs +66 -0
- package/lib/embedded-contract/triage.mjs +134 -0
- package/lib/embedded-contract/workflow-defs.mjs +125 -0
- package/lib/embedded-contract/workflow-invoke.mjs +218 -0
- package/lib/hooks/config-protection.mjs +12 -5
- package/lib/init-unified.mjs +36 -26
- package/lib/intake/classify.mjs +6 -0
- package/lib/intake/tables/rnd.mjs +33 -0
- package/lib/mcp/server.mjs +69 -0
- package/lib/mcp/tools/embedded-contract.mjs +77 -0
- package/lib/model-router.mjs +40 -0
- package/lib/op-log.mjs +61 -0
- package/lib/roles/catalog.mjs +26 -95
- package/lib/service-manager.mjs +33 -11
- package/lib/setup.mjs +21 -3
- package/lib/validators/skills.mjs +21 -0
- package/package.json +9 -3
- package/scripts/sync-specialists.mjs +12 -3
- package/skills/ai/agent-dev.md +2 -0
- package/skills/ai/llm-security.md +2 -0
- package/skills/ai/ml-ops.md +2 -0
- package/skills/ai/orchestration-workflow.md +2 -0
- package/skills/ai/prompt-and-eval.md +2 -0
- package/skills/ai/prompt-optimizer.md +2 -0
- package/skills/ai/rag-system.md +2 -0
- package/skills/ai/trace-triage.md +36 -0
- package/skills/architecture/api-design.md +2 -0
- package/skills/architecture/caching.md +2 -0
- package/skills/architecture/cloud-native.md +2 -0
- package/skills/architecture/message-queue.md +2 -0
- package/skills/architecture/security-arch.md +2 -0
- package/skills/compliance/ai-disclosure.md +2 -0
- package/skills/compliance/data-privacy.md +2 -0
- package/skills/compliance/license-audit.md +2 -0
- package/skills/compliance/regulatory-review.md +2 -0
- package/skills/development/cpp.md +2 -0
- package/skills/development/go.md +2 -0
- package/skills/development/java.md +2 -0
- package/skills/development/kotlin.md +2 -0
- package/skills/development/mobile-crossplatform.md +2 -0
- package/skills/development/python.md +2 -0
- package/skills/development/rust.md +2 -0
- package/skills/development/shell.md +2 -0
- package/skills/development/swift.md +2 -0
- package/skills/development/typescript.md +2 -0
- package/skills/devops/ci-cd.md +2 -0
- package/skills/devops/containerization.md +2 -0
- package/skills/devops/cost-optimization.md +2 -0
- package/skills/devops/data-engineering.md +2 -0
- package/skills/devops/database.md +2 -0
- package/skills/devops/dependency-management.md +2 -0
- package/skills/devops/devsecops.md +2 -0
- package/skills/devops/git-workflow.md +2 -0
- package/skills/devops/incident-response.md +2 -0
- package/skills/devops/monorepo.md +2 -0
- package/skills/devops/observability.md +2 -0
- package/skills/devops/performance.md +2 -0
- package/skills/devops/testing.md +2 -0
- package/skills/docs/adr-workflow.md +2 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -0
- package/skills/docs/customer-profile-workflow.md +2 -0
- package/skills/docs/document-ingest-workflow.md +2 -0
- package/skills/docs/evidence-ingest-workflow.md +2 -0
- package/skills/docs/init-docs.md +2 -0
- package/skills/docs/init-project.md +2 -0
- package/skills/docs/memo-and-decision-capture.md +45 -0
- package/skills/docs/prd-workflow.md +2 -0
- package/skills/docs/prfaq-workflow.md +2 -0
- package/skills/docs/product-intelligence-review.md +2 -0
- package/skills/docs/product-intelligence-workflow.md +2 -0
- package/skills/docs/product-signal-workflow.md +2 -0
- package/skills/docs/research-workflow.md +2 -0
- package/skills/docs/runbook-workflow.md +2 -0
- package/skills/docs/strategy-workflow.md +2 -0
- package/skills/docs/transcript-synthesis.md +43 -0
- package/skills/exploration/dependency-graph-reading.md +2 -0
- package/skills/exploration/repo-map.md +2 -0
- package/skills/exploration/tracer-bullet-method.md +2 -0
- package/skills/exploration/unknown-codebase-onboarding.md +2 -0
- package/skills/frameworks/django.md +2 -0
- package/skills/frameworks/nextjs.md +2 -0
- package/skills/frameworks/react.md +2 -0
- package/skills/frameworks/spring-boot.md +2 -0
- package/skills/frontend-design/accessibility.md +2 -0
- package/skills/frontend-design/component-patterns.md +2 -0
- package/skills/frontend-design/engineering.md +2 -0
- package/skills/frontend-design/screen-reader-testing.md +34 -0
- package/skills/frontend-design/state-management.md +2 -0
- package/skills/frontend-design/ui-aesthetics.md +2 -0
- package/skills/frontend-design/ux-principles.md +2 -0
- package/skills/operating/change-management.md +2 -0
- package/skills/operating/incident-response.md +2 -0
- package/skills/operating/oncall-rotation.md +2 -0
- package/skills/operating/orchestration-reference.md +2 -0
- package/skills/operating/raw-data-structuring.md +44 -0
- package/skills/operating/unstructured-triage.md +45 -0
- package/skills/quality-gates/premortem.md +37 -0
- package/skills/quality-gates/review-work.md +2 -0
- package/skills/quality-gates/verify-change.md +2 -0
- package/skills/quality-gates/verify-module.md +2 -0
- package/skills/quality-gates/verify-quality.md +2 -0
- package/skills/quality-gates/verify-security.md +2 -0
- package/skills/roles/architect.ai-systems.md +2 -0
- package/skills/roles/architect.data.md +2 -0
- package/skills/roles/architect.enterprise.md +2 -0
- package/skills/roles/architect.integration.md +2 -0
- package/skills/roles/architect.md +2 -0
- package/skills/roles/architect.platform.md +2 -0
- package/skills/roles/data-analyst.experiment.md +2 -0
- package/skills/roles/data-analyst.md +2 -0
- package/skills/roles/data-analyst.product-intelligence.md +2 -0
- package/skills/roles/data-analyst.product.md +2 -0
- package/skills/roles/data-analyst.telemetry.md +2 -0
- package/skills/roles/data-engineer.pipeline.md +2 -0
- package/skills/roles/data-engineer.vector-retrieval.md +2 -0
- package/skills/roles/data-engineer.warehouse.md +2 -0
- package/skills/roles/debugger.md +2 -0
- package/skills/roles/designer.accessibility.md +2 -0
- package/skills/roles/designer.md +2 -0
- package/skills/roles/engineer.ai.md +2 -0
- package/skills/roles/engineer.data.md +2 -0
- package/skills/roles/engineer.md +2 -0
- package/skills/roles/engineer.platform.md +2 -0
- package/skills/roles/operator.docs.md +2 -0
- package/skills/roles/operator.md +2 -0
- package/skills/roles/operator.release.md +2 -0
- package/skills/roles/operator.sre.md +2 -0
- package/skills/roles/orchestrator.md +2 -0
- package/skills/roles/product-manager.ai-product.md +2 -0
- package/skills/roles/product-manager.business-strategy.md +2 -0
- package/skills/roles/product-manager.enterprise.md +2 -0
- package/skills/roles/product-manager.growth.md +2 -0
- package/skills/roles/product-manager.md +2 -0
- package/skills/roles/product-manager.platform.md +2 -0
- package/skills/roles/product-manager.product.md +2 -0
- package/skills/roles/qa.ai-eval.md +2 -0
- package/skills/roles/qa.api-contract.md +2 -0
- package/skills/roles/qa.data-pipeline.md +2 -0
- package/skills/roles/qa.md +2 -0
- package/skills/roles/qa.test-automation.md +2 -0
- package/skills/roles/qa.web-ui.md +2 -0
- package/skills/roles/researcher.explorer.md +2 -0
- package/skills/roles/researcher.md +2 -0
- package/skills/roles/researcher.ux.md +2 -0
- package/skills/roles/reviewer.devil-advocate.md +2 -0
- package/skills/roles/reviewer.evaluator.md +2 -0
- package/skills/roles/reviewer.md +2 -0
- package/skills/roles/reviewer.trace.md +2 -0
- package/skills/roles/security.ai.md +2 -0
- package/skills/roles/security.appsec.md +2 -0
- package/skills/roles/security.cloud.md +2 -0
- package/skills/roles/security.legal-compliance.md +2 -0
- package/skills/roles/security.md +2 -0
- package/skills/roles/security.privacy.md +2 -0
- package/skills/roles/security.supply-chain.md +2 -0
- package/skills/security/blue-team.md +2 -0
- package/skills/security/code-audit.md +2 -0
- package/skills/security/pentest.md +2 -0
- package/skills/security/red-team.md +2 -0
- package/skills/security/threat-intel.md +2 -0
- package/skills/security/vuln-research.md +2 -0
- package/skills/strategy/competitive-landscape.md +2 -0
- package/skills/strategy/jobs-to-be-done.md +38 -0
- package/skills/strategy/market-research-methods.md +2 -0
- package/skills/strategy/narrative-arc.md +2 -0
- package/skills/strategy/pricing-positioning.md +2 -0
- package/skills/utility/clean-code.md +2 -0
- package/specialists/prompts/cx-engineer.md +1 -1
- package/specialists/registry.json +18 -9
- package/specialists/role-manifests.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/redaction.mjs — structural no-secrets guard for contract output.
|
|
3
|
+
*
|
|
4
|
+
* Every embedded-contract response passes through this module before
|
|
5
|
+
* serialization. `redact` masks values at secret-looking keys; `assertNoSecrets`
|
|
6
|
+
* is the load-bearing guard — it throws if any live credential value (drawn from
|
|
7
|
+
* the environment by key name) appears anywhere in the payload, so a contract
|
|
8
|
+
* surface can never leak a token even if a future field copies one in by
|
|
9
|
+
* mistake. The check is generic and decoupled from the provider registry: it
|
|
10
|
+
* derives the secret set from env keys at call time rather than a hardcoded list.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const SECRET_KEY_RE = /(api[_-]?key|secret|token|password|passwd|credential|authorization|bearer|cookie|session[_-]?id|private[_-]?key|client[_-]?secret|access[_-]?key)/i;
|
|
14
|
+
|
|
15
|
+
const REDACTED = '[redacted]';
|
|
16
|
+
|
|
17
|
+
// Credential env values shorter than this are treated as non-secret (empty
|
|
18
|
+
// flags, "1", port numbers) and excluded so the guard does not false-positive.
|
|
19
|
+
|
|
20
|
+
const MIN_SECRET_LENGTH = 8;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Deep-clone `value`, masking any value whose key name looks like a secret.
|
|
24
|
+
*
|
|
25
|
+
* @param {*} value
|
|
26
|
+
* @returns {*}
|
|
27
|
+
*/
|
|
28
|
+
export function redact(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.map((v) => redact(v));
|
|
30
|
+
if (value && typeof value === 'object') {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, v] of Object.entries(value)) {
|
|
33
|
+
if (SECRET_KEY_RE.test(k) && (typeof v === 'string' || typeof v === 'number')) {
|
|
34
|
+
out[k] = REDACTED;
|
|
35
|
+
} else {
|
|
36
|
+
out[k] = redact(v);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Collect non-trivial environment values whose key name looks like a secret.
|
|
46
|
+
*
|
|
47
|
+
* @param {Record<string,string>} [env]
|
|
48
|
+
* @returns {Set<string>}
|
|
49
|
+
*/
|
|
50
|
+
export function collectSecretValues(env = process.env) {
|
|
51
|
+
const out = new Set();
|
|
52
|
+
for (const [key, val] of Object.entries(env || {})) {
|
|
53
|
+
if (typeof val === 'string' && val.length >= MIN_SECRET_LENGTH && SECRET_KEY_RE.test(key)) {
|
|
54
|
+
out.add(val);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function walkStrings(value, visit, pathStr = '$') {
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
visit(value, pathStr);
|
|
63
|
+
} else if (Array.isArray(value)) {
|
|
64
|
+
value.forEach((v, i) => walkStrings(v, visit, `${pathStr}[${i}]`));
|
|
65
|
+
} else if (value && typeof value === 'object') {
|
|
66
|
+
for (const [k, v] of Object.entries(value)) walkStrings(v, visit, `${pathStr}.${k}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Throw if any live credential value appears as a substring of any string in
|
|
72
|
+
* the payload. Returns the payload unchanged when clean.
|
|
73
|
+
*
|
|
74
|
+
* @param {*} value
|
|
75
|
+
* @param {{env?:Record<string,string>}} [opts]
|
|
76
|
+
* @returns {*}
|
|
77
|
+
*/
|
|
78
|
+
export function assertNoSecrets(value, { env = process.env } = {}) {
|
|
79
|
+
const secrets = collectSecretValues(env);
|
|
80
|
+
if (secrets.size === 0) return value;
|
|
81
|
+
const leaks = [];
|
|
82
|
+
walkStrings(value, (str, pathStr) => {
|
|
83
|
+
for (const secret of secrets) {
|
|
84
|
+
if (str.includes(secret)) leaks.push(pathStr);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
if (leaks.length) {
|
|
88
|
+
throw new Error(`Secret value leaked into contract output at: ${[...new Set(leaks)].join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/role-facts.mjs — shared role/contract enrichment.
|
|
3
|
+
*
|
|
4
|
+
* Both the triage/planning and workflow-invocation contracts need the same
|
|
5
|
+
* facts about a role chain: per-role rationale, the union of declared skills,
|
|
6
|
+
* and the evidence a role requires plus the outputs it must produce. Those facts
|
|
7
|
+
* come only from verifiable sources — the role catalog (registry) and the
|
|
8
|
+
* specialist contracts — so this module centralizes the lookups and keeps the
|
|
9
|
+
* two contracts consistent and free of invented requirements.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { listRoles } from '../roles/catalog.mjs';
|
|
13
|
+
import { getIncomingContracts } from '../specialist-contracts.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Map of role id → catalog descriptor for the current registry.
|
|
17
|
+
* @returns {Map<string, object>}
|
|
18
|
+
*/
|
|
19
|
+
export function roleMap() {
|
|
20
|
+
return new Map(listRoles().map((role) => [role.id, role]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-role rationale for a chain, drawn from each role's catalog description.
|
|
25
|
+
*
|
|
26
|
+
* @param {string[]} chain
|
|
27
|
+
* @param {Map<string,object>} [map]
|
|
28
|
+
* @returns {Array<{role:string, reason:string}>}
|
|
29
|
+
*/
|
|
30
|
+
export function roleRationale(chain, map = roleMap()) {
|
|
31
|
+
return chain.map((id) => ({
|
|
32
|
+
role: `cx-${id}`,
|
|
33
|
+
reason: map.get(id)?.description || 'No role description available.',
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Union of declared skills across a role chain, de-duplicated.
|
|
39
|
+
*
|
|
40
|
+
* @param {string[]} chain
|
|
41
|
+
* @param {Map<string,object>} [map]
|
|
42
|
+
* @returns {string[]}
|
|
43
|
+
*/
|
|
44
|
+
export function skillsForChain(chain, map = roleMap()) {
|
|
45
|
+
return [...new Set(chain.flatMap((id) => map.get(id)?.skills || []))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Evidence a role requires and the outputs it must produce, taken from the
|
|
50
|
+
* role's incoming specialist contract (input.mustContain + preconditions /
|
|
51
|
+
* output.mustContain). Returns empty arrays when no contract is declared.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} roleId Bare role id (no cx- prefix).
|
|
54
|
+
* @returns {{evidenceRequirements:string[], expectedOutputs:string[]}}
|
|
55
|
+
*/
|
|
56
|
+
export function contractFacts(roleId) {
|
|
57
|
+
const contracts = getIncomingContracts(`cx-${roleId}`) || [];
|
|
58
|
+
const evidence = new Set();
|
|
59
|
+
const outputs = new Set();
|
|
60
|
+
for (const c of contracts) {
|
|
61
|
+
for (const item of c.input?.mustContain || []) evidence.add(item);
|
|
62
|
+
for (const pre of c.preconditions || []) evidence.add(pre);
|
|
63
|
+
for (const item of c.output?.mustContain || []) outputs.add(item);
|
|
64
|
+
}
|
|
65
|
+
return { evidenceRequirements: [...evidence], expectedOutputs: [...outputs] };
|
|
66
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/triage.mjs — embedded triage and planning contract.
|
|
3
|
+
*
|
|
4
|
+
* Classifies an artifact and returns a role-aware plan without enqueuing or
|
|
5
|
+
* executing anything. Wraps the deterministic classifier (lib/intake/classify)
|
|
6
|
+
* and enriches it from real sources only: the role catalog supplies role
|
|
7
|
+
* rationale and skills, the specialist contracts supply evidence requirements
|
|
8
|
+
* and expected outputs. Classification confidence (how sure the classifier is)
|
|
9
|
+
* is reported as a distinct, labeled field from any downstream generation
|
|
10
|
+
* confidence, which is produced only when a workflow actually runs.
|
|
11
|
+
*
|
|
12
|
+
* Pure and side-effect-free: no queue write, no disk artifact, no model call.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { classifyRdIntake } from '../intake/classify.mjs';
|
|
16
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
17
|
+
import { roleMap, roleRationale, skillsForChain, contractFacts } from './role-facts.mjs';
|
|
18
|
+
import { workflowTypeForIntake } from './workflow-defs.mjs';
|
|
19
|
+
|
|
20
|
+
const CONFIDENCE_FLOOR = 0.6;
|
|
21
|
+
|
|
22
|
+
function buildRiskFactors(triage) {
|
|
23
|
+
const factors = [];
|
|
24
|
+
if (triage.intakeType === 'unknown') factors.push('classification is unknown — no keywords matched');
|
|
25
|
+
if (typeof triage.confidence === 'number' && triage.confidence < CONFIDENCE_FLOOR) {
|
|
26
|
+
factors.push(`low classification confidence (${triage.confidence.toFixed(2)})`);
|
|
27
|
+
}
|
|
28
|
+
const ambiguous = (triage.rationale || '').includes('ambiguous');
|
|
29
|
+
if (ambiguous) factors.push('top two classifications are close (ambiguous)');
|
|
30
|
+
return factors;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Produce a role-aware plan for an artifact without enqueuing or executing it.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} request
|
|
37
|
+
* @param {string} [request.input] Artifact text to classify.
|
|
38
|
+
* @param {string} [request.sourcePath] Filename/source hint for the classifier.
|
|
39
|
+
* @param {string} [request.artifactType] Optional artifact-type hint (advisory).
|
|
40
|
+
* @param {string} [request.domain]
|
|
41
|
+
* @param {string} [request.desiredOutcome]
|
|
42
|
+
* @param {string[]} [request.constraints]
|
|
43
|
+
* @param {string[]} [request.availableRoles] Restrict the plan to these role ids.
|
|
44
|
+
* @param {object} [request.profile]
|
|
45
|
+
* @param {object} [opts] { env, cwd }
|
|
46
|
+
* @returns {object}
|
|
47
|
+
*/
|
|
48
|
+
export function recommendPlan(request = {}, { env = process.env, cwd = process.cwd() } = {}) {
|
|
49
|
+
const { input = '', sourcePath = '', artifactType, availableRoles, profile = null, ingestion = null } = request;
|
|
50
|
+
const warnings = [];
|
|
51
|
+
|
|
52
|
+
// Surface extraction provenance and flag low-yield/truncated inputs so a
|
|
53
|
+
// sparse classification is attributable to thin source text, not silent loss.
|
|
54
|
+
|
|
55
|
+
if (ingestion) {
|
|
56
|
+
if (ingestion.error) warnings.push(`ingestion: ${ingestion.error.code} — ${ingestion.error.reason}`);
|
|
57
|
+
for (const drop of ingestion.droppedInfo || []) {
|
|
58
|
+
warnings.push(`ingestion: ${drop.kind || 'dropped-content'}${drop.recoverable ? ' (recoverable)' : ''}.`);
|
|
59
|
+
}
|
|
60
|
+
if (ingestion.truncated) warnings.push('ingestion: source text was truncated before classification.');
|
|
61
|
+
if (ingestion.note) warnings.push(`ingestion: ${ingestion.note}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const triage = classifyRdIntake({ sourcePath: sourcePath || (artifactType ? `${artifactType}.md` : ''), extractedText: input, profile });
|
|
65
|
+
const roles = roleMap();
|
|
66
|
+
|
|
67
|
+
let chain = [...(triage.recommendedChain || [])];
|
|
68
|
+
const primaryOwner = triage.primaryOwner;
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(availableRoles) && availableRoles.length) {
|
|
71
|
+
const allowed = new Set(availableRoles);
|
|
72
|
+
const dropped = chain.filter((r) => !allowed.has(r));
|
|
73
|
+
if (dropped.length) warnings.push(`Roles not in availableRoles were dropped from the chain: ${dropped.join(', ')}.`);
|
|
74
|
+
chain = chain.filter((r) => allowed.has(r));
|
|
75
|
+
if (primaryOwner && !allowed.has(primaryOwner)) {
|
|
76
|
+
warnings.push(`Primary owner ${primaryOwner} is not in availableRoles; the plan cannot be executed as recommended.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rationale = roleRationale(chain, roles);
|
|
81
|
+
const suggestedSkills = skillsForChain(chain, roles);
|
|
82
|
+
|
|
83
|
+
const { evidenceRequirements, expectedOutputs } = contractFacts(primaryOwner);
|
|
84
|
+
if (!evidenceRequirements.length) {
|
|
85
|
+
warnings.push(`No declared evidence contract for cx-${primaryOwner}; evidence requirements are unspecified.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const deploymentMode = getDeploymentMode(env, { cwd });
|
|
89
|
+
const requiresApproval = Boolean(triage.requiresApproval) || deploymentMode === 'enterprise';
|
|
90
|
+
const approvalRequirements = {
|
|
91
|
+
requiresApproval,
|
|
92
|
+
reason: triage.requiresApproval
|
|
93
|
+
? 'The classified work type requires approval before durable changes.'
|
|
94
|
+
: (deploymentMode === 'enterprise' ? 'Enterprise deployment mode mandates approval for durable changes.' : 'No approval required before proposing changes.'),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ownerAvailable = !primaryOwner || !Array.isArray(availableRoles) || !availableRoles.length || availableRoles.includes(primaryOwner);
|
|
98
|
+
const canExecute = triage.intakeType !== 'unknown'
|
|
99
|
+
&& typeof triage.confidence === 'number' && triage.confidence >= CONFIDENCE_FLOOR
|
|
100
|
+
&& chain.length > 0
|
|
101
|
+
&& ownerAvailable;
|
|
102
|
+
const canExecuteReason = canExecute
|
|
103
|
+
? 'Classification is confident and maps to a role chain that can be invoked.'
|
|
104
|
+
: 'Execution is not recommended: classification is unknown, low-confidence, or no usable role chain remains.';
|
|
105
|
+
|
|
106
|
+
const nextStepOptions = [
|
|
107
|
+
{ action: 'enqueue', description: 'Add to the intake queue for processing (construct intake process).' },
|
|
108
|
+
];
|
|
109
|
+
if (canExecute) nextStepOptions.push({ action: 'invoke-workflow', description: 'Invoke the recommended role chain as an embedded workflow.' });
|
|
110
|
+
if (!canExecute) nextStepOptions.push({ action: 'clarify', description: 'Request more detail; classification confidence is low or unknown.' });
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
classification: { intakeType: triage.intakeType, rdStage: triage.rdStage },
|
|
114
|
+
ingestion,
|
|
115
|
+
confidenceKind: 'classification',
|
|
116
|
+
classificationConfidence: triage.confidence,
|
|
117
|
+
primaryOwner,
|
|
118
|
+
recommendedAction: triage.recommendedAction,
|
|
119
|
+
recommendedChain: chain,
|
|
120
|
+
suggestedWorkflowType: workflowTypeForIntake(triage.intakeType),
|
|
121
|
+
roleRationale: rationale,
|
|
122
|
+
suggestedSkills,
|
|
123
|
+
evidenceRequirements,
|
|
124
|
+
expectedOutputs,
|
|
125
|
+
approvalRequirements,
|
|
126
|
+
risks: { level: triage.risk || 'unknown', factors: buildRiskFactors(triage) },
|
|
127
|
+
nextStepOptions,
|
|
128
|
+
canExecute,
|
|
129
|
+
canExecuteReason,
|
|
130
|
+
rationale: triage.rationale,
|
|
131
|
+
candidates: triage.candidates || [],
|
|
132
|
+
warnings,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/workflow-defs.mjs — embedded workflow type definitions.
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for the workflow types an embedding application can
|
|
5
|
+
* invoke. Each definition names a default role chain (real registry role ids), a
|
|
6
|
+
* model tier, a default approval mode, an optional output-schema artifact, and a
|
|
7
|
+
* one-line description. Skills are not hardcoded here — they are derived from the
|
|
8
|
+
* selected roles' declared skills (role-facts) so this file never names a skill
|
|
9
|
+
* id that does not exist. Capability discovery reads these definitions so the
|
|
10
|
+
* published workflow list cannot drift from what invocation actually supports.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DEFS = {
|
|
14
|
+
'evidence-ingest': {
|
|
15
|
+
tier: 'fast',
|
|
16
|
+
defaultApprovalMode: 'proposal-only',
|
|
17
|
+
chain: ['researcher', 'data-analyst'],
|
|
18
|
+
outputSchema: null,
|
|
19
|
+
description: 'Ingest and structure raw evidence (notes, documents, signals) into a normalized summary.',
|
|
20
|
+
},
|
|
21
|
+
'proposal-review': {
|
|
22
|
+
tier: 'standard',
|
|
23
|
+
defaultApprovalMode: 'requires-human-approval',
|
|
24
|
+
chain: ['reviewer', 'devil-advocate'],
|
|
25
|
+
outputSchema: 'review-report',
|
|
26
|
+
description: 'Review a proposal for correctness, risk, and hidden assumptions before acceptance.',
|
|
27
|
+
},
|
|
28
|
+
'prd-draft': {
|
|
29
|
+
tier: 'standard',
|
|
30
|
+
defaultApprovalMode: 'proposal-only',
|
|
31
|
+
chain: ['product-manager', 'architect'],
|
|
32
|
+
outputSchema: 'decision',
|
|
33
|
+
description: 'Draft a product requirements document from a problem statement and supporting evidence.',
|
|
34
|
+
},
|
|
35
|
+
'architecture-review': {
|
|
36
|
+
tier: 'reasoning',
|
|
37
|
+
defaultApprovalMode: 'requires-human-approval',
|
|
38
|
+
chain: ['architect', 'security', 'devil-advocate'],
|
|
39
|
+
outputSchema: 'review-report',
|
|
40
|
+
description: 'Review an architecture or design for trade-offs, failure modes, and security exposure.',
|
|
41
|
+
},
|
|
42
|
+
'risk-review': {
|
|
43
|
+
tier: 'reasoning',
|
|
44
|
+
defaultApprovalMode: 'requires-human-approval',
|
|
45
|
+
chain: ['devil-advocate', 'security', 'legal-compliance'],
|
|
46
|
+
outputSchema: 'review-report',
|
|
47
|
+
description: 'Stress-test a plan for risk: failure modes, security, and compliance exposure.',
|
|
48
|
+
},
|
|
49
|
+
'research-synthesis': {
|
|
50
|
+
tier: 'reasoning',
|
|
51
|
+
defaultApprovalMode: 'proposal-only',
|
|
52
|
+
chain: ['researcher', 'data-analyst', 'evaluator'],
|
|
53
|
+
outputSchema: null,
|
|
54
|
+
description: 'Synthesize multiple sources into a cited, evidence-graded research summary.',
|
|
55
|
+
},
|
|
56
|
+
'transcript-process': {
|
|
57
|
+
tier: 'fast',
|
|
58
|
+
defaultApprovalMode: 'proposal-only',
|
|
59
|
+
chain: ['researcher', 'data-analyst'],
|
|
60
|
+
outputSchema: null,
|
|
61
|
+
description: 'Process a meeting/call transcript into a summary, decisions, and action items.',
|
|
62
|
+
},
|
|
63
|
+
'data-structure': {
|
|
64
|
+
tier: 'standard',
|
|
65
|
+
defaultApprovalMode: 'proposal-only',
|
|
66
|
+
chain: ['data-analyst', 'data-engineer'],
|
|
67
|
+
outputSchema: null,
|
|
68
|
+
description: 'Parse, validate, and profile a raw dataset into a structured, described shape.',
|
|
69
|
+
},
|
|
70
|
+
'memo-draft': {
|
|
71
|
+
tier: 'fast',
|
|
72
|
+
defaultApprovalMode: 'proposal-only',
|
|
73
|
+
chain: ['docs-keeper', 'reviewer'],
|
|
74
|
+
outputSchema: null,
|
|
75
|
+
description: 'Draft a decision or status memo from a problem statement and context.',
|
|
76
|
+
},
|
|
77
|
+
'structure-notes': {
|
|
78
|
+
tier: 'fast',
|
|
79
|
+
defaultApprovalMode: 'proposal-only',
|
|
80
|
+
chain: ['orchestrator', 'researcher'],
|
|
81
|
+
outputSchema: null,
|
|
82
|
+
description: 'Structure an unclassified brain-dump or rough notes into a normalized summary with extracted intents.',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Maps a classifier intakeType to the workflow type that would carry it out, so
|
|
87
|
+
// the triage contract can suggest a directly-invokable workflow. Returns null
|
|
88
|
+
// when no workflow covers the classification (the plan is not directly invokable).
|
|
89
|
+
|
|
90
|
+
const INTAKE_TO_WORKFLOW = {
|
|
91
|
+
proposal: 'proposal-review',
|
|
92
|
+
prd: 'prd-draft',
|
|
93
|
+
'meta-prd': 'prd-draft',
|
|
94
|
+
architecture: 'architecture-review',
|
|
95
|
+
rfc: 'architecture-review',
|
|
96
|
+
risk: 'risk-review',
|
|
97
|
+
security: 'risk-review',
|
|
98
|
+
research: 'research-synthesis',
|
|
99
|
+
'research-note': 'research-synthesis',
|
|
100
|
+
signal: 'evidence-ingest',
|
|
101
|
+
'user-signal': 'evidence-ingest',
|
|
102
|
+
evidence: 'evidence-ingest',
|
|
103
|
+
memo: 'memo-draft',
|
|
104
|
+
transcript: 'transcript-process',
|
|
105
|
+
'raw-data': 'data-structure',
|
|
106
|
+
unknown: 'structure-notes',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const WORKFLOW_TYPES = Object.keys(DEFS);
|
|
110
|
+
|
|
111
|
+
export function getWorkflowDef(type) {
|
|
112
|
+
return DEFS[type] || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Public, secret-free description of every workflow type for capability discovery.
|
|
117
|
+
* @returns {Array<object>}
|
|
118
|
+
*/
|
|
119
|
+
export function listWorkflowDefs() {
|
|
120
|
+
return WORKFLOW_TYPES.map((type) => ({ type, ...DEFS[type] }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function workflowTypeForIntake(intakeType) {
|
|
124
|
+
return INTAKE_TO_WORKFLOW[intakeType] || null;
|
|
125
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/workflow-invoke.mjs — embedded workflow invocation contract.
|
|
3
|
+
*
|
|
4
|
+
* Invokes a named Construct workflow non-interactively and returns a provenanced
|
|
5
|
+
* execution plan: the selected roles (auto/explicit/constrained), each role's
|
|
6
|
+
* rationale, the skills they apply, the resolved model, the evidence the chain
|
|
7
|
+
* requires, and the output contract it will produce. Construct supplies the
|
|
8
|
+
* orchestration plan and provenance; the host agent runtime performs the
|
|
9
|
+
* specialist reasoning — so this contract never fabricates specialist output.
|
|
10
|
+
*
|
|
11
|
+
* Durable side effects are gated by approval mode (resolveWriteGate):
|
|
12
|
+
* - proposal-only → status 'proposed', zero writes
|
|
13
|
+
* - requires-human-approval → status 'awaiting-approval', an approval request
|
|
14
|
+
* is recorded, zero durable writes
|
|
15
|
+
* - allow-durable-write → status 'recorded', the invocation is written to
|
|
16
|
+
* the observation store as durable provenance
|
|
17
|
+
* On team/enterprise deployments, durable writes are flagged as mandatorily
|
|
18
|
+
* audited. A traceId correlates the call with downstream provenance.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
24
|
+
import { addObservation } from '../observation-store.mjs';
|
|
25
|
+
import { recordApprovalRequest } from '../roles/approval-surface.mjs';
|
|
26
|
+
import { getWorkflowDef, WORKFLOW_TYPES } from './workflow-defs.mjs';
|
|
27
|
+
import { roleMap, roleRationale, skillsForChain, contractFacts } from './role-facts.mjs';
|
|
28
|
+
import { resolveEmbeddedModel } from './model-resolve.mjs';
|
|
29
|
+
import { resolveWriteGate, newTraceId, DEFAULT_APPROVAL_MODE } from './audit.mjs';
|
|
30
|
+
|
|
31
|
+
const VALID_STRATEGIES = ['auto', 'explicit', 'constrained'];
|
|
32
|
+
|
|
33
|
+
function selectRoles({ strategy, defChain, requestedRoles, map, warnings }) {
|
|
34
|
+
const filterKnown = (ids) => ids.filter((id) => {
|
|
35
|
+
if (map.has(id)) return true;
|
|
36
|
+
warnings.push(`Unknown role id ignored: ${id}.`);
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (strategy === 'explicit') {
|
|
41
|
+
if (!Array.isArray(requestedRoles) || requestedRoles.length === 0) {
|
|
42
|
+
warnings.push('roleStrategy=explicit requires requestedRoles; using the workflow default chain.');
|
|
43
|
+
return [...defChain];
|
|
44
|
+
}
|
|
45
|
+
return filterKnown(requestedRoles);
|
|
46
|
+
}
|
|
47
|
+
if (strategy === 'constrained') {
|
|
48
|
+
const allowed = new Set(requestedRoles || []);
|
|
49
|
+
const dropped = defChain.filter((id) => !allowed.has(id));
|
|
50
|
+
if (dropped.length) warnings.push(`Constrained: roles outside requestedRoles dropped from the default chain: ${dropped.join(', ')}.`);
|
|
51
|
+
return defChain.filter((id) => allowed.has(id));
|
|
52
|
+
}
|
|
53
|
+
return [...defChain];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function errorResult({ workflowId, traceId, approvalMode, code, message, warnings }) {
|
|
57
|
+
return {
|
|
58
|
+
workflowId,
|
|
59
|
+
status: 'error',
|
|
60
|
+
selectedRoles: [],
|
|
61
|
+
roleStrategy: null,
|
|
62
|
+
roleRationale: [],
|
|
63
|
+
skillsApplied: [],
|
|
64
|
+
modelResolution: null,
|
|
65
|
+
outputs: null,
|
|
66
|
+
recommendations: [],
|
|
67
|
+
evidence: { requirements: [], satisfied: [], traceId },
|
|
68
|
+
risks: { level: 'unknown', factors: [] },
|
|
69
|
+
requiresApproval: false,
|
|
70
|
+
approvalMode: approvalMode || DEFAULT_APPROVAL_MODE,
|
|
71
|
+
durableWritesPerformed: [],
|
|
72
|
+
traceId,
|
|
73
|
+
errors: [{ code, message }],
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Invoke an embedded workflow. Async because durable provenance and approval
|
|
80
|
+
* records are written through existing stores. Returns a result object carrying
|
|
81
|
+
* a `warnings` array (lifted into the envelope by the calling surface).
|
|
82
|
+
*
|
|
83
|
+
* @param {object} request
|
|
84
|
+
* @param {string} request.workflowType
|
|
85
|
+
* @param {string} [request.input]
|
|
86
|
+
* @param {object} [request.context]
|
|
87
|
+
* @param {string} [request.roleStrategy] auto | explicit | constrained
|
|
88
|
+
* @param {string[]} [request.requestedRoles]
|
|
89
|
+
* @param {string} [request.approvalMode] proposal-only | requires-human-approval | allow-durable-write
|
|
90
|
+
* @param {boolean} [request.trace=true]
|
|
91
|
+
* @param {string} [request.host]
|
|
92
|
+
* @param {string} [request.hostModel]
|
|
93
|
+
* @param {string} [request.hostProvider]
|
|
94
|
+
* @param {object} [opts] { env, cwd }
|
|
95
|
+
* @returns {Promise<object>}
|
|
96
|
+
*/
|
|
97
|
+
export async function invokeWorkflow(request = {}, { env = process.env, cwd = process.cwd() } = {}) {
|
|
98
|
+
const {
|
|
99
|
+
workflowType, context = {}, roleStrategy = 'auto', requestedRoles,
|
|
100
|
+
approvalMode, trace = true, host, hostModel, hostProvider, ingestion = null,
|
|
101
|
+
} = request;
|
|
102
|
+
const warnings = [];
|
|
103
|
+
if (ingestion) {
|
|
104
|
+
if (ingestion.error) warnings.push(`ingestion: ${ingestion.error.code} — ${ingestion.error.reason}`);
|
|
105
|
+
for (const drop of ingestion.droppedInfo || []) warnings.push(`ingestion: ${drop.kind || 'dropped-content'}${drop.recoverable ? ' (recoverable)' : ''}.`);
|
|
106
|
+
if (ingestion.truncated) warnings.push('ingestion: source text was truncated before invocation.');
|
|
107
|
+
if (ingestion.note) warnings.push(`ingestion: ${ingestion.note}`);
|
|
108
|
+
}
|
|
109
|
+
const traceId = trace ? newTraceId() : null;
|
|
110
|
+
const workflowId = `wf-${randomUUID()}`;
|
|
111
|
+
|
|
112
|
+
const def = getWorkflowDef(workflowType);
|
|
113
|
+
if (!def) {
|
|
114
|
+
return errorResult({ workflowId, traceId, approvalMode, code: 'UNKNOWN_WORKFLOW_TYPE', message: `Unknown workflowType "${workflowType}". Known: ${WORKFLOW_TYPES.join(', ')}.`, warnings });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const strategy = VALID_STRATEGIES.includes(roleStrategy) ? roleStrategy : 'auto';
|
|
118
|
+
if (strategy !== roleStrategy) warnings.push(`Unknown roleStrategy "${roleStrategy}"; defaulting to auto.`);
|
|
119
|
+
|
|
120
|
+
const map = roleMap();
|
|
121
|
+
const selectedRoles = selectRoles({ strategy, defChain: def.chain, requestedRoles, map, warnings });
|
|
122
|
+
if (selectedRoles.length === 0) {
|
|
123
|
+
return errorResult({ workflowId, traceId, approvalMode, code: 'NO_ROLES_SELECTED', message: 'Role selection produced an empty chain.', warnings });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { warnings: modelWarnings = [], ...modelResolution } = resolveEmbeddedModel(
|
|
127
|
+
{ workflowType, requestedTier: def.tier, host, hostModel, hostProvider },
|
|
128
|
+
{ env },
|
|
129
|
+
);
|
|
130
|
+
for (const w of modelWarnings) warnings.push(`model-resolution: ${w}`);
|
|
131
|
+
|
|
132
|
+
const primaryOwner = selectedRoles[0];
|
|
133
|
+
const facts = contractFacts(primaryOwner);
|
|
134
|
+
const skillsApplied = skillsForChain(selectedRoles, map);
|
|
135
|
+
const rationale = roleRationale(selectedRoles, map);
|
|
136
|
+
|
|
137
|
+
// Evidence is satisfied only when a requirement key is actually present in the
|
|
138
|
+
// caller-supplied context; missing requirements are reported, never assumed.
|
|
139
|
+
|
|
140
|
+
const contextKeys = new Set(Object.keys(context || {}));
|
|
141
|
+
const satisfied = facts.evidenceRequirements.filter((req) => contextKeys.has(req));
|
|
142
|
+
const missingEvidence = facts.evidenceRequirements.filter((req) => !contextKeys.has(req));
|
|
143
|
+
|
|
144
|
+
const deploymentMode = getDeploymentMode(env, { cwd });
|
|
145
|
+
const mode = approvalMode || def.defaultApprovalMode;
|
|
146
|
+
const gate = resolveWriteGate({ approvalMode: mode, deploymentMode });
|
|
147
|
+
|
|
148
|
+
const outputs = {
|
|
149
|
+
schema: def.outputSchema,
|
|
150
|
+
expected: facts.expectedOutputs,
|
|
151
|
+
note: 'Construct returns the orchestration plan and output contract; specialist reasoning is performed by the host agent runtime.',
|
|
152
|
+
};
|
|
153
|
+
const recommendations = [`Run roles in order: ${selectedRoles.map((r) => `cx-${r}`).join(' → ')}.`];
|
|
154
|
+
if (missingEvidence.length) recommendations.push(`Supply missing evidence before execution: ${missingEvidence.join(', ')}.`);
|
|
155
|
+
|
|
156
|
+
const riskFactors = [];
|
|
157
|
+
if (missingEvidence.length) riskFactors.push(`missing required evidence: ${missingEvidence.join(', ')}`);
|
|
158
|
+
if (modelResolution.error) riskFactors.push(`model could not be resolved: ${modelResolution.error.reason}`);
|
|
159
|
+
const risks = { level: missingEvidence.length || modelResolution.error ? 'medium' : 'low', factors: riskFactors };
|
|
160
|
+
|
|
161
|
+
const durableWritesPerformed = [];
|
|
162
|
+
const errors = [];
|
|
163
|
+
let status;
|
|
164
|
+
|
|
165
|
+
if (gate.requiresApproval) {
|
|
166
|
+
try {
|
|
167
|
+
await recordApprovalRequest({
|
|
168
|
+
personaId: primaryOwner,
|
|
169
|
+
action: `embedded-workflow:${workflowType}`,
|
|
170
|
+
target: workflowId,
|
|
171
|
+
reason: `Embedded workflow ${workflowType} awaiting human approval.`,
|
|
172
|
+
context: { workflowType, selectedRoles, traceId },
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
warnings.push(`Could not record approval request: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
status = 'awaiting-approval';
|
|
178
|
+
} else if (gate.allowWrites) {
|
|
179
|
+
try {
|
|
180
|
+
const obs = await addObservation(cwd, {
|
|
181
|
+
role: primaryOwner,
|
|
182
|
+
category: 'decision',
|
|
183
|
+
summary: `Embedded workflow invoked: ${workflowType}`,
|
|
184
|
+
content: `roles=${selectedRoles.join(',')}; tier=${def.tier}; model=${modelResolution.selectedModel || 'unresolved'}; traceId=${traceId}`,
|
|
185
|
+
tags: ['embedded-contract', `workflow/${workflowType}`],
|
|
186
|
+
source: 'embedded-contract',
|
|
187
|
+
});
|
|
188
|
+
durableWritesPerformed.push({ kind: 'observation', id: obs?.id ?? null, store: '.cx/observations', audited: gate.mandatoryAudit });
|
|
189
|
+
} catch (err) {
|
|
190
|
+
errors.push({ code: 'DURABLE_WRITE_FAILED', message: err.message });
|
|
191
|
+
}
|
|
192
|
+
status = errors.length ? 'error' : 'recorded';
|
|
193
|
+
} else {
|
|
194
|
+
status = 'proposed';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
workflowId,
|
|
199
|
+
workflowType,
|
|
200
|
+
status,
|
|
201
|
+
ingestion,
|
|
202
|
+
selectedRoles,
|
|
203
|
+
roleStrategy: strategy,
|
|
204
|
+
roleRationale: rationale,
|
|
205
|
+
skillsApplied,
|
|
206
|
+
modelResolution,
|
|
207
|
+
outputs,
|
|
208
|
+
recommendations,
|
|
209
|
+
evidence: { requirements: facts.evidenceRequirements, satisfied, missing: missingEvidence, traceId },
|
|
210
|
+
risks,
|
|
211
|
+
requiresApproval: gate.requiresApproval,
|
|
212
|
+
approvalMode: gate.approvalMode,
|
|
213
|
+
durableWritesPerformed,
|
|
214
|
+
traceId,
|
|
215
|
+
errors,
|
|
216
|
+
warnings,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* prettier / tsconfig / biome / stylelint configs. Those rules are a contract
|
|
7
7
|
* with the codebase; weakening them is almost always a bug fix in disguise.
|
|
8
8
|
*
|
|
9
|
-
* Meta-system files (hooks, registry, settings template, install
|
|
10
|
-
* audited but NOT blocked. Editing them is normal
|
|
9
|
+
* Meta-system files (hooks, registry, settings template, install/setup
|
|
10
|
+
* entrypoints) are audited but NOT blocked. Editing them is normal work on
|
|
11
11
|
* Construct itself. The previous block-with-CX_ALLOW_META_EDIT pattern was a
|
|
12
12
|
* sticky env-var toggle: friction the first time, zero protection thereafter
|
|
13
13
|
* because it stayed on. Layers that actually work for self-modification
|
|
@@ -31,22 +31,29 @@ const filePath = process.env.TOOL_INPUT_FILE_PATH || (() => {
|
|
|
31
31
|
|
|
32
32
|
if (!filePath) process.exit(0);
|
|
33
33
|
|
|
34
|
+
// Lint-RULE configs are hard contracts with the codebase: weakening a rule is
|
|
35
|
+
// almost always a bug fix in disguise, so edits are blocked. tsconfig is the TS
|
|
36
|
+
// COMPILER config — its options legitimately change with the toolchain (e.g. a
|
|
37
|
+
// version-compat flag like ignoreDeprecations on a major TS bump), so it is
|
|
38
|
+
// audited as a meta edit rather than blocked. Quality-relevant tsconfig changes
|
|
39
|
+
// still surface in code review and the type-checked build.
|
|
34
40
|
const PROTECTED = [
|
|
35
41
|
/\.eslintrc(\.[a-z]+)?$/i,
|
|
36
42
|
/eslint\.config(\.[a-z]+)?$/i,
|
|
37
43
|
/\.prettierrc(\.[a-z]+)?$/i,
|
|
38
44
|
/prettier\.config(\.[a-z]+)?$/i,
|
|
39
|
-
/tsconfig(\.[^/]+)?\.json$/i,
|
|
40
45
|
/biome\.json$/i,
|
|
41
46
|
/\.stylelintrc(\.[a-z]+)?$/i,
|
|
42
47
|
/stylelint\.config(\.[a-z]+)?$/i,
|
|
43
48
|
];
|
|
44
49
|
|
|
45
50
|
const META_FILES = [
|
|
46
|
-
/(?:^|\/)
|
|
47
|
-
/(?:^|\/)
|
|
51
|
+
/(?:^|\/)specialists\/registry\.json$/,
|
|
52
|
+
/(?:^|\/)lib\/setup\.mjs$/,
|
|
53
|
+
/(?:^|\/)bin\/construct-postinstall\.mjs$/,
|
|
48
54
|
/(?:^|\/)claude\/settings\.template\.json$/,
|
|
49
55
|
/(?:^|\/)lib\/hooks\/[^/]+\.mjs$/,
|
|
56
|
+
/(?:^|\/)tsconfig(\.[^/]+)?\.json$/i,
|
|
50
57
|
];
|
|
51
58
|
|
|
52
59
|
async function emitEvent(type, category, summary) {
|