@geraldmaron/construct 1.0.15 → 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 +19 -3
- package/bin/construct +376 -98
- package/bin/construct-postinstall.mjs +17 -2
- package/db/schema/010_cx_scores.sql +51 -0
- package/lib/cli-commands.mjs +333 -147
- package/lib/contracts/validate.mjs +20 -10
- package/lib/contracts/violation-log.mjs +27 -12
- 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/hooks/stop-notify.mjs +7 -0
- package/lib/init-unified.mjs +36 -26
- package/lib/install/first-invocation.mjs +5 -1
- package/lib/intake/classify.mjs +6 -0
- package/lib/intake/prepare.mjs +22 -4
- package/lib/intake/tables/rnd.mjs +33 -0
- package/lib/integrations/intake-integrations.mjs +28 -2
- package/lib/mcp/server.mjs +69 -0
- package/lib/mcp/tools/embedded-contract.mjs +77 -0
- package/lib/mcp/tools/telemetry.mjs +30 -0
- package/lib/model-router.mjs +40 -0
- package/lib/op-log.mjs +61 -0
- package/lib/orchestration-policy.mjs +15 -0
- package/lib/roles/catalog.mjs +26 -95
- package/lib/roles/gateway.mjs +30 -1
- package/lib/scheduler/index.mjs +24 -4
- package/lib/server/insights.mjs +12 -0
- package/lib/service-manager.mjs +33 -11
- package/lib/setup.mjs +73 -10
- package/lib/update.mjs +31 -3
- package/lib/upgrade.mjs +31 -4
- package/lib/validators/skills.mjs +21 -0
- package/package.json +9 -3
- package/personas/construct.md +2 -0
- package/platforms/claude/CLAUDE.md +43 -15
- package/scripts/sync-specialists.mjs +32 -5
- 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,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) {
|
|
@@ -26,6 +26,7 @@ import { listSessions, loadSession, updateSession } from '../session-store.mjs';
|
|
|
26
26
|
import { captureSessionArtifacts } from '../artifact-capture.mjs';
|
|
27
27
|
import { appendSessionStats } from '../memory-stats.mjs';
|
|
28
28
|
import { estimateUsageCost } from '../telemetry/model-pricing-catalog.mjs';
|
|
29
|
+
import { flushReadTrackerDeltas } from '../read-tracker-store.mjs';
|
|
29
30
|
|
|
30
31
|
function loadTranscriptCheckpoints(checkpointPath) {
|
|
31
32
|
try {
|
|
@@ -140,6 +141,12 @@ let raw = '';
|
|
|
140
141
|
try { raw = readFileSync(0, 'utf8'); } catch { /* non-critical */ }
|
|
141
142
|
if (raw) process.stdout.write(raw);
|
|
142
143
|
|
|
144
|
+
// PostToolUse Read hooks append delta lines instead of rewriting the full
|
|
145
|
+
// read-tracker JSON on every call. Stop fires once per session end; this
|
|
146
|
+
// is the canonical point to fold accumulated deltas back into the JSON.
|
|
147
|
+
// Already wired into pre-compact + audit-reads; Stop is the third anchor.
|
|
148
|
+
try { flushReadTrackerDeltas({ env: process.env }); } catch { /* non-critical */ }
|
|
149
|
+
|
|
143
150
|
const home = homedir();
|
|
144
151
|
const tsResultPath = join(home, '.cx', 'ts-result.txt');
|
|
145
152
|
const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
|
package/lib/init-unified.mjs
CHANGED
|
@@ -283,20 +283,26 @@ function parseWithDocs(value) {
|
|
|
283
283
|
return LANE_ORDER;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
286
|
+
// Map singular aliases to canonical lane keys BEFORE validating, so
|
|
287
|
+
// `--with-docs=adr,rfc` resolves rather than being silently filtered out.
|
|
288
|
+
const ALIASES = { adr: 'adrs', rfc: 'rfcs', runbook: 'runbooks', postmortem: 'postmortems', prd: 'prds', brief: 'briefs', memo: 'memos', changelog: 'changelogs', meeting: 'meetings', note: 'notes' };
|
|
289
|
+
|
|
290
|
+
const requested = value.split(',').map(v => v.trim().toLowerCase()).filter(Boolean);
|
|
291
|
+
const resolved = [];
|
|
292
|
+
const unknown = [];
|
|
293
|
+
for (const lane of requested) {
|
|
294
|
+
const canonical = ALIASES[lane] || lane;
|
|
295
|
+
if (LANE_ORDER.includes(canonical)) resolved.push(canonical);
|
|
296
|
+
else unknown.push(lane);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Surface typo'd lanes loudly instead of dropping them silently; valid lanes
|
|
300
|
+
// still proceed so one bad token does not abort init.
|
|
301
|
+
if (unknown.length) {
|
|
302
|
+
console.warn(`Warning: ignoring unknown --with-docs lane(s): ${unknown.join(', ')}. Valid lanes: ${LANE_ORDER.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return [...new Set(resolved)]; // dedupe
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
function inferProjectName(targetPath) {
|
|
@@ -741,8 +747,6 @@ This project enforces documentation quality:
|
|
|
741
747
|
}
|
|
742
748
|
|
|
743
749
|
function preflight(target) {
|
|
744
|
-
console.log('[TRACE init:pre-flight]');
|
|
745
|
-
|
|
746
750
|
// Check git repo
|
|
747
751
|
try {
|
|
748
752
|
execSync('git rev-parse --show-toplevel', { cwd: target, stdio: 'ignore' });
|
|
@@ -797,6 +801,14 @@ async function main() {
|
|
|
797
801
|
|
|
798
802
|
const { clean } = preflight(target);
|
|
799
803
|
|
|
804
|
+
// Interactive sessions check machine prerequisites up front and offer to run
|
|
805
|
+
// `construct install` before any scaffolding, so a missing dependency is
|
|
806
|
+
// surfaced at the start rather than midway. Non-interactive runs rely on
|
|
807
|
+
// preflight's git check and stay silent so CI/tests are unaffected.
|
|
808
|
+
if (interactive && process.stdin.isTTY) {
|
|
809
|
+
await checkPrerequisites();
|
|
810
|
+
}
|
|
811
|
+
|
|
800
812
|
if (!quiet) {
|
|
801
813
|
console.log(`Initializing Construct in ${path.relative(process.cwd(), target) || "."}`);
|
|
802
814
|
}
|
|
@@ -1037,7 +1049,6 @@ async function main() {
|
|
|
1037
1049
|
// so the user can inspect what's watched and edit the file directly. Empty
|
|
1038
1050
|
// parentDirs is the correct default: the inbox watcher always scans
|
|
1039
1051
|
// .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
|
|
1040
|
-
console.log('[TRACE init:intake-ask]');
|
|
1041
1052
|
|
|
1042
1053
|
const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
|
|
1043
1054
|
// When a custom intake surface was detected, default includeProjectInbox
|
|
@@ -1063,8 +1074,6 @@ async function main() {
|
|
|
1063
1074
|
}
|
|
1064
1075
|
|
|
1065
1076
|
// Ask about documentation system
|
|
1066
|
-
console.log('[TRACE init:docs-ask]');
|
|
1067
|
-
|
|
1068
1077
|
const { lanes, withArchitecture, withReadme, docsPreset: userDocsPreset } = await askDocumentationQuestions();
|
|
1069
1078
|
|
|
1070
1079
|
// Create README.md if requested or missing
|
|
@@ -1072,9 +1081,7 @@ async function main() {
|
|
|
1072
1081
|
if (withReadme || !fs.existsSync(readmePath)) {
|
|
1073
1082
|
writeIfMissing(readmePath, buildProjectReadme(projectName));
|
|
1074
1083
|
}
|
|
1075
|
-
|
|
1076
|
-
console.log('[TRACE init:docs-write]');
|
|
1077
|
-
|
|
1084
|
+
|
|
1078
1085
|
// Create documentation system if lanes specified
|
|
1079
1086
|
if (lanes.length > 0) {
|
|
1080
1087
|
// Filter out lanes that the project already covers elsewhere
|
|
@@ -1195,19 +1202,22 @@ async function main() {
|
|
|
1195
1202
|
console.log(' NEXT STEPS');
|
|
1196
1203
|
console.log('═══════════════════════════════════════════════════════════');
|
|
1197
1204
|
console.log('');
|
|
1198
|
-
|
|
1205
|
+
// Number steps from a running counter so the list stays sequential when
|
|
1206
|
+
// the optional intake step is absent (was: hard-coded 1,2,4 → a visible gap).
|
|
1207
|
+
let step = 0;
|
|
1208
|
+
console.log(`${++step}. Review AGENTS.md`);
|
|
1199
1209
|
console.log(' Operating rules and guidelines for this project');
|
|
1200
1210
|
console.log('');
|
|
1201
|
-
console.log(
|
|
1211
|
+
console.log(`${++step}. Edit plan.md`);
|
|
1202
1212
|
console.log(' Add your current work and tasks');
|
|
1203
1213
|
console.log('');
|
|
1204
1214
|
if (lanes.includes('intake')) {
|
|
1205
|
-
console.log(
|
|
1215
|
+
console.log(`${++step}. Use Intake`);
|
|
1206
1216
|
console.log(' Drop files in .cx/inbox/ for processing');
|
|
1207
1217
|
console.log(' Run: construct intake');
|
|
1208
1218
|
console.log('');
|
|
1209
1219
|
}
|
|
1210
|
-
console.log(
|
|
1220
|
+
console.log(`${++step}. Start working`);
|
|
1211
1221
|
console.log(' Address @construct in your editor to begin');
|
|
1212
1222
|
console.log('');
|
|
1213
1223
|
}
|
|
@@ -24,8 +24,12 @@ import { parseEnvFile, writeEnvValues, getUserEnvPath } from '../env-config.mjs'
|
|
|
24
24
|
import { probeAll, formatProbe } from '../bootstrap/resources.mjs';
|
|
25
25
|
|
|
26
26
|
const BOOTSTRAP_CHECKED_KEY = 'BOOTSTRAP_CHECKED';
|
|
27
|
+
// 'install' is the actual handler name in bin/construct's dispatch map;
|
|
28
|
+
// 'setup' is kept for backwards-compat in case anything still routes via
|
|
29
|
+
// that key. Without 'install' the probe will re-prompt the user while
|
|
30
|
+
// they're already running the very command meant to satisfy it.
|
|
27
31
|
const SKIP_COMMANDS = new Set([
|
|
28
|
-
'hook', 'setup', 'uninstall', 'version', 'help', 'completions', 'doctor',
|
|
32
|
+
'hook', 'install', 'setup', 'uninstall', 'version', 'help', 'completions', 'doctor', 'upgrade', 'update',
|
|
29
33
|
]);
|
|
30
34
|
|
|
31
35
|
export function shouldSkipProbe({ command, env = process.env, stdin = process.stdin } = {}) {
|
package/lib/intake/classify.mjs
CHANGED
|
@@ -90,6 +90,9 @@ const FILENAME_HINTS = [
|
|
|
90
90
|
{ re: /eval|metric|benchmark/i, intakeType: 'eval-finding' },
|
|
91
91
|
{ re: /-bug\b|\bbug-/i, intakeType: 'bug' },
|
|
92
92
|
{ re: /runbook/i, intakeType: 'ops' },
|
|
93
|
+
{ re: /^memo[-_]|[-_]memo\b/i, intakeType: 'memo' },
|
|
94
|
+
{ re: /transcript|meeting-notes|standup|stand-up/i, intakeType: 'transcript' },
|
|
95
|
+
{ re: /dataset|raw-?data|data-?dump|\.csv$|\.tsv$/i, intakeType: 'raw-data' },
|
|
93
96
|
];
|
|
94
97
|
|
|
95
98
|
// Title-level family terms by intakeType. When the doc's H1 contains a
|
|
@@ -105,6 +108,9 @@ const TITLE_FAMILY = {
|
|
|
105
108
|
'eval-finding': /^#\s*(eval(uation)?(\s+finding)?\b|benchmark)/im,
|
|
106
109
|
bug: /^#\s*(bug\s+report|defect)\b/im,
|
|
107
110
|
ops: /^#\s*(runbook|operations\s+guide)\b/im,
|
|
111
|
+
memo: /^#\s*(memo|decision\s+memo|status\s+update)\b/im,
|
|
112
|
+
transcript: /^#\s*(transcript|meeting\s+(notes|minutes)|call\s+notes)\b/im,
|
|
113
|
+
'raw-data': /^#\s*(dataset|raw\s+data|data\s+export)\b/im,
|
|
108
114
|
};
|
|
109
115
|
|
|
110
116
|
// Title-lock returns the highest confidence the classifier emits because
|
package/lib/intake/prepare.mjs
CHANGED
|
@@ -32,12 +32,13 @@ import path from 'node:path';
|
|
|
32
32
|
import { buildHybridSearchResultsAsync } from '../storage/hybrid-query.mjs';
|
|
33
33
|
import { suggestDocsLaneForFile } from '../docs-routing.mjs';
|
|
34
34
|
import { createIntakeQueue } from './queue.mjs';
|
|
35
|
-
import { classifyRdIntake } from './classify.mjs';
|
|
35
|
+
import { classifyRdIntake, suggestTags } from './classify.mjs';
|
|
36
36
|
import { detectCustomerMentions, linkSignalToCustomer, updateCustomerProfile } from '../embed/customer-profiles.mjs';
|
|
37
37
|
import { resolveActiveProfile } from '../profiles/loader.mjs';
|
|
38
38
|
import { emitBestEffort as emitRoleEvent } from '../roles/event-bus.mjs';
|
|
39
39
|
import { gatherAttribution, stampAttribution } from './attribution.mjs';
|
|
40
40
|
import { MANIFEST_REL_PATH } from './manifest.mjs';
|
|
41
|
+
import { loadVocabulary } from '../tags/vocabulary.mjs';
|
|
41
42
|
|
|
42
43
|
const DEFAULT_RELATED_LIMIT = 5;
|
|
43
44
|
const EXCERPT_CHARS = 800;
|
|
@@ -66,12 +67,18 @@ export async function prepareIntakeForIngestedFile({
|
|
|
66
67
|
|
|
67
68
|
let related = [];
|
|
68
69
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const raw = await hybridSearchFn(rootDir, query, { limit: relatedLimit, env });
|
|
71
|
+
// Test mocks return an array directly; production wrapper returns
|
|
72
|
+
// { results: [...] }. Accept both shapes so suggestTags sees the
|
|
73
|
+
// same `related` regardless of caller. Preserve `tags` for the
|
|
74
|
+
// related-inherit suggestion path.
|
|
75
|
+
const hits = Array.isArray(raw) ? raw : (raw?.results || []);
|
|
76
|
+
related = hits.map((hit) => ({
|
|
77
|
+
path: hit.source_path || hit.path || hit.id,
|
|
72
78
|
title: hit.title || hit.id,
|
|
73
79
|
score: hit.score,
|
|
74
80
|
summary: hit.summary || '',
|
|
81
|
+
tags: Array.isArray(hit.tags) ? hit.tags : undefined,
|
|
75
82
|
}));
|
|
76
83
|
} catch (err) {
|
|
77
84
|
related = [];
|
|
@@ -98,6 +105,16 @@ export async function prepareIntakeForIngestedFile({
|
|
|
98
105
|
|
|
99
106
|
const droppedInfo = ingestedFile.droppedInfo ?? [];
|
|
100
107
|
|
|
108
|
+
// Tag auto-attribution: deterministic suggestions from triage + related
|
|
109
|
+
// doc inheritance, filtered against the project tag vocabulary. Vocab
|
|
110
|
+
// load failures fall through with empty suggestions — never block the
|
|
111
|
+
// packet write on a missing/malformed vocab file.
|
|
112
|
+
|
|
113
|
+
let vocab = null;
|
|
114
|
+
try { vocab = loadVocabulary(rootDir); }
|
|
115
|
+
catch { vocab = null; }
|
|
116
|
+
const tagSuggestions = suggestTags(triage, related, vocab);
|
|
117
|
+
|
|
101
118
|
const baseEntry = {
|
|
102
119
|
intake: {
|
|
103
120
|
sourcePath: ingestedFile.sourcePath,
|
|
@@ -112,6 +129,7 @@ export async function prepareIntakeForIngestedFile({
|
|
|
112
129
|
excerpt: extracted.slice(0, EXCERPT_CHARS),
|
|
113
130
|
query,
|
|
114
131
|
customers: customers.length ? customers : undefined,
|
|
132
|
+
tags: tagSuggestions.length > 0 ? tagSuggestions : undefined,
|
|
115
133
|
};
|
|
116
134
|
|
|
117
135
|
// Capability detection mirrors inbox.mjs: stamp provenance onto the packet
|
|
@@ -22,6 +22,9 @@ export const INTAKE_TYPES = [
|
|
|
22
22
|
'ops',
|
|
23
23
|
'security',
|
|
24
24
|
'legal-compliance',
|
|
25
|
+
'memo',
|
|
26
|
+
'transcript',
|
|
27
|
+
'raw-data',
|
|
25
28
|
'unknown',
|
|
26
29
|
];
|
|
27
30
|
|
|
@@ -170,6 +173,36 @@ export const CLASSIFICATION_TABLE = [
|
|
|
170
173
|
risk: 'low',
|
|
171
174
|
requiresApproval: false,
|
|
172
175
|
},
|
|
176
|
+
{
|
|
177
|
+
intakeType: 'memo',
|
|
178
|
+
keywords: ['memo', 'decision memo', 'for your information', 'fyi', 'action item', 'action items', 'status update', 'weekly update', 'announcement', 'heads up', 'team update', 'decided to', 'proposal to'],
|
|
179
|
+
rdStage: 'artifact',
|
|
180
|
+
primaryOwner: 'docs-keeper',
|
|
181
|
+
recommendedChain: ['docs-keeper', 'reviewer'],
|
|
182
|
+
recommendedAction: 'summarize',
|
|
183
|
+
risk: 'low',
|
|
184
|
+
requiresApproval: false,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
intakeType: 'transcript',
|
|
188
|
+
keywords: ['transcript', 'webvtt', 'meeting notes', 'meeting minutes', 'minutes of', 'attendees', 'call notes', 'stand-up notes', 'standup notes', 'recording of', 'speaker 1', 'speaker 2'],
|
|
189
|
+
rdStage: 'signal',
|
|
190
|
+
primaryOwner: 'researcher',
|
|
191
|
+
recommendedChain: ['researcher', 'data-analyst'],
|
|
192
|
+
recommendedAction: 'summarize',
|
|
193
|
+
risk: 'low',
|
|
194
|
+
requiresApproval: false,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
intakeType: 'raw-data',
|
|
198
|
+
keywords: ['dataset', 'raw data', 'data dump', 'csv export', 'data export', 'column names', 'field names', 'rows and columns', 'records export', 'telemetry export', 'json export'],
|
|
199
|
+
rdStage: 'research',
|
|
200
|
+
primaryOwner: 'data-analyst',
|
|
201
|
+
recommendedChain: ['data-analyst', 'data-engineer'],
|
|
202
|
+
recommendedAction: 'summarize',
|
|
203
|
+
risk: 'low',
|
|
204
|
+
requiresApproval: false,
|
|
205
|
+
},
|
|
173
206
|
];
|
|
174
207
|
|
|
175
208
|
export default { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE };
|
|
@@ -88,6 +88,22 @@ function resolveConfluenceAuth(homeDir) {
|
|
|
88
88
|
|
|
89
89
|
// ── GitHub Issues ────────────────────────────────────────────────────────
|
|
90
90
|
|
|
91
|
+
// A packet that originated from a demo/fixture/inbox-test source must not
|
|
92
|
+
// be published to a real GitHub repo. The caller can override via
|
|
93
|
+
// `publishDemo: true` when the demo run is intentional (e.g. an explicit
|
|
94
|
+
// integration test that creates and then deletes the issue). Default-safe.
|
|
95
|
+
|
|
96
|
+
export function isDemoIntakePacket(packet) {
|
|
97
|
+
if (process.env.CONSTRUCT_DEMO === '1') return true;
|
|
98
|
+
if (process.env.CONSTRUCT_INTAKE_DEMO === '1') return true;
|
|
99
|
+
const sourcePath = String(packet?.intake?.sourcePath || packet?.sourcePath || '');
|
|
100
|
+
if (!sourcePath) return false;
|
|
101
|
+
if (sourcePath.includes('/tests/fixtures/')) return true;
|
|
102
|
+
if (sourcePath.includes('/.cx/intake/demo/')) return true;
|
|
103
|
+
if (sourcePath.includes('/cx-intake-demo')) return true;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
/**
|
|
92
108
|
* Create a GitHub issue from an intake packet.
|
|
93
109
|
*
|
|
@@ -98,9 +114,19 @@ function resolveConfluenceAuth(homeDir) {
|
|
|
98
114
|
* @param {string} [opts.apiUrl] - GitHub API URL (default: api.github.com)
|
|
99
115
|
* @param {string} [opts.host] - GitHub host (default: github.com)
|
|
100
116
|
* @param {object} [opts.fetchImpl]
|
|
101
|
-
* @
|
|
117
|
+
* @param {boolean} [opts.publishDemo] - Override the demo-source gate
|
|
118
|
+
* @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string, skipped?: string }>}
|
|
102
119
|
*/
|
|
103
|
-
export async function createGitHubIssue(packet, { repo, token, apiUrl, host, fetchImpl = globalThis.fetch } = {}) {
|
|
120
|
+
export async function createGitHubIssue(packet, { repo, token, apiUrl, host, fetchImpl = globalThis.fetch, publishDemo = false } = {}) {
|
|
121
|
+
if (!publishDemo && isDemoIntakePacket(packet)) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
externalUrl: null,
|
|
125
|
+
externalId: null,
|
|
126
|
+
skipped: 'demo-source',
|
|
127
|
+
error: 'Refused to publish: packet originated from a demo/fixture path (set CONSTRUCT_DEMO=0 and re-run with --publish-issues to override).',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
104
130
|
const hd = homedir();
|
|
105
131
|
const auth = resolveGitHubAuth(hd);
|
|
106
132
|
const resolvedRepo = repo || auth.repo || process.env.GITHUB_REPO;
|