@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.
Files changed (198) hide show
  1. package/README.md +19 -3
  2. package/bin/construct +376 -98
  3. package/bin/construct-postinstall.mjs +17 -2
  4. package/db/schema/010_cx_scores.sql +51 -0
  5. package/lib/cli-commands.mjs +333 -147
  6. package/lib/contracts/validate.mjs +20 -10
  7. package/lib/contracts/violation-log.mjs +27 -12
  8. package/lib/embedded-contract/audit.mjs +52 -0
  9. package/lib/embedded-contract/capability.mjs +179 -0
  10. package/lib/embedded-contract/contract-version.mjs +39 -0
  11. package/lib/embedded-contract/envelope.mjs +70 -0
  12. package/lib/embedded-contract/index.mjs +71 -0
  13. package/lib/embedded-contract/ingest.mjs +77 -0
  14. package/lib/embedded-contract/model-resolve.mjs +186 -0
  15. package/lib/embedded-contract/redaction.mjs +91 -0
  16. package/lib/embedded-contract/role-facts.mjs +66 -0
  17. package/lib/embedded-contract/triage.mjs +134 -0
  18. package/lib/embedded-contract/workflow-defs.mjs +125 -0
  19. package/lib/embedded-contract/workflow-invoke.mjs +218 -0
  20. package/lib/hooks/config-protection.mjs +12 -5
  21. package/lib/hooks/stop-notify.mjs +7 -0
  22. package/lib/init-unified.mjs +36 -26
  23. package/lib/install/first-invocation.mjs +5 -1
  24. package/lib/intake/classify.mjs +6 -0
  25. package/lib/intake/prepare.mjs +22 -4
  26. package/lib/intake/tables/rnd.mjs +33 -0
  27. package/lib/integrations/intake-integrations.mjs +28 -2
  28. package/lib/mcp/server.mjs +69 -0
  29. package/lib/mcp/tools/embedded-contract.mjs +77 -0
  30. package/lib/mcp/tools/telemetry.mjs +30 -0
  31. package/lib/model-router.mjs +40 -0
  32. package/lib/op-log.mjs +61 -0
  33. package/lib/orchestration-policy.mjs +15 -0
  34. package/lib/roles/catalog.mjs +26 -95
  35. package/lib/roles/gateway.mjs +30 -1
  36. package/lib/scheduler/index.mjs +24 -4
  37. package/lib/server/insights.mjs +12 -0
  38. package/lib/service-manager.mjs +33 -11
  39. package/lib/setup.mjs +73 -10
  40. package/lib/update.mjs +31 -3
  41. package/lib/upgrade.mjs +31 -4
  42. package/lib/validators/skills.mjs +21 -0
  43. package/package.json +9 -3
  44. package/personas/construct.md +2 -0
  45. package/platforms/claude/CLAUDE.md +43 -15
  46. package/scripts/sync-specialists.mjs +32 -5
  47. package/skills/ai/agent-dev.md +2 -0
  48. package/skills/ai/llm-security.md +2 -0
  49. package/skills/ai/ml-ops.md +2 -0
  50. package/skills/ai/orchestration-workflow.md +2 -0
  51. package/skills/ai/prompt-and-eval.md +2 -0
  52. package/skills/ai/prompt-optimizer.md +2 -0
  53. package/skills/ai/rag-system.md +2 -0
  54. package/skills/ai/trace-triage.md +36 -0
  55. package/skills/architecture/api-design.md +2 -0
  56. package/skills/architecture/caching.md +2 -0
  57. package/skills/architecture/cloud-native.md +2 -0
  58. package/skills/architecture/message-queue.md +2 -0
  59. package/skills/architecture/security-arch.md +2 -0
  60. package/skills/compliance/ai-disclosure.md +2 -0
  61. package/skills/compliance/data-privacy.md +2 -0
  62. package/skills/compliance/license-audit.md +2 -0
  63. package/skills/compliance/regulatory-review.md +2 -0
  64. package/skills/development/cpp.md +2 -0
  65. package/skills/development/go.md +2 -0
  66. package/skills/development/java.md +2 -0
  67. package/skills/development/kotlin.md +2 -0
  68. package/skills/development/mobile-crossplatform.md +2 -0
  69. package/skills/development/python.md +2 -0
  70. package/skills/development/rust.md +2 -0
  71. package/skills/development/shell.md +2 -0
  72. package/skills/development/swift.md +2 -0
  73. package/skills/development/typescript.md +2 -0
  74. package/skills/devops/ci-cd.md +2 -0
  75. package/skills/devops/containerization.md +2 -0
  76. package/skills/devops/cost-optimization.md +2 -0
  77. package/skills/devops/data-engineering.md +2 -0
  78. package/skills/devops/database.md +2 -0
  79. package/skills/devops/dependency-management.md +2 -0
  80. package/skills/devops/devsecops.md +2 -0
  81. package/skills/devops/git-workflow.md +2 -0
  82. package/skills/devops/incident-response.md +2 -0
  83. package/skills/devops/monorepo.md +2 -0
  84. package/skills/devops/observability.md +2 -0
  85. package/skills/devops/performance.md +2 -0
  86. package/skills/devops/testing.md +2 -0
  87. package/skills/docs/adr-workflow.md +2 -0
  88. package/skills/docs/backlog-proposal-workflow.md +2 -0
  89. package/skills/docs/customer-profile-workflow.md +2 -0
  90. package/skills/docs/document-ingest-workflow.md +2 -0
  91. package/skills/docs/evidence-ingest-workflow.md +2 -0
  92. package/skills/docs/init-docs.md +2 -0
  93. package/skills/docs/init-project.md +2 -0
  94. package/skills/docs/memo-and-decision-capture.md +45 -0
  95. package/skills/docs/prd-workflow.md +2 -0
  96. package/skills/docs/prfaq-workflow.md +2 -0
  97. package/skills/docs/product-intelligence-review.md +2 -0
  98. package/skills/docs/product-intelligence-workflow.md +2 -0
  99. package/skills/docs/product-signal-workflow.md +2 -0
  100. package/skills/docs/research-workflow.md +2 -0
  101. package/skills/docs/runbook-workflow.md +2 -0
  102. package/skills/docs/strategy-workflow.md +2 -0
  103. package/skills/docs/transcript-synthesis.md +43 -0
  104. package/skills/exploration/dependency-graph-reading.md +2 -0
  105. package/skills/exploration/repo-map.md +2 -0
  106. package/skills/exploration/tracer-bullet-method.md +2 -0
  107. package/skills/exploration/unknown-codebase-onboarding.md +2 -0
  108. package/skills/frameworks/django.md +2 -0
  109. package/skills/frameworks/nextjs.md +2 -0
  110. package/skills/frameworks/react.md +2 -0
  111. package/skills/frameworks/spring-boot.md +2 -0
  112. package/skills/frontend-design/accessibility.md +2 -0
  113. package/skills/frontend-design/component-patterns.md +2 -0
  114. package/skills/frontend-design/engineering.md +2 -0
  115. package/skills/frontend-design/screen-reader-testing.md +34 -0
  116. package/skills/frontend-design/state-management.md +2 -0
  117. package/skills/frontend-design/ui-aesthetics.md +2 -0
  118. package/skills/frontend-design/ux-principles.md +2 -0
  119. package/skills/operating/change-management.md +2 -0
  120. package/skills/operating/incident-response.md +2 -0
  121. package/skills/operating/oncall-rotation.md +2 -0
  122. package/skills/operating/orchestration-reference.md +2 -0
  123. package/skills/operating/raw-data-structuring.md +44 -0
  124. package/skills/operating/unstructured-triage.md +45 -0
  125. package/skills/quality-gates/premortem.md +37 -0
  126. package/skills/quality-gates/review-work.md +2 -0
  127. package/skills/quality-gates/verify-change.md +2 -0
  128. package/skills/quality-gates/verify-module.md +2 -0
  129. package/skills/quality-gates/verify-quality.md +2 -0
  130. package/skills/quality-gates/verify-security.md +2 -0
  131. package/skills/roles/architect.ai-systems.md +2 -0
  132. package/skills/roles/architect.data.md +2 -0
  133. package/skills/roles/architect.enterprise.md +2 -0
  134. package/skills/roles/architect.integration.md +2 -0
  135. package/skills/roles/architect.md +2 -0
  136. package/skills/roles/architect.platform.md +2 -0
  137. package/skills/roles/data-analyst.experiment.md +2 -0
  138. package/skills/roles/data-analyst.md +2 -0
  139. package/skills/roles/data-analyst.product-intelligence.md +2 -0
  140. package/skills/roles/data-analyst.product.md +2 -0
  141. package/skills/roles/data-analyst.telemetry.md +2 -0
  142. package/skills/roles/data-engineer.pipeline.md +2 -0
  143. package/skills/roles/data-engineer.vector-retrieval.md +2 -0
  144. package/skills/roles/data-engineer.warehouse.md +2 -0
  145. package/skills/roles/debugger.md +2 -0
  146. package/skills/roles/designer.accessibility.md +2 -0
  147. package/skills/roles/designer.md +2 -0
  148. package/skills/roles/engineer.ai.md +2 -0
  149. package/skills/roles/engineer.data.md +2 -0
  150. package/skills/roles/engineer.md +2 -0
  151. package/skills/roles/engineer.platform.md +2 -0
  152. package/skills/roles/operator.docs.md +2 -0
  153. package/skills/roles/operator.md +2 -0
  154. package/skills/roles/operator.release.md +2 -0
  155. package/skills/roles/operator.sre.md +2 -0
  156. package/skills/roles/orchestrator.md +2 -0
  157. package/skills/roles/product-manager.ai-product.md +2 -0
  158. package/skills/roles/product-manager.business-strategy.md +2 -0
  159. package/skills/roles/product-manager.enterprise.md +2 -0
  160. package/skills/roles/product-manager.growth.md +2 -0
  161. package/skills/roles/product-manager.md +2 -0
  162. package/skills/roles/product-manager.platform.md +2 -0
  163. package/skills/roles/product-manager.product.md +2 -0
  164. package/skills/roles/qa.ai-eval.md +2 -0
  165. package/skills/roles/qa.api-contract.md +2 -0
  166. package/skills/roles/qa.data-pipeline.md +2 -0
  167. package/skills/roles/qa.md +2 -0
  168. package/skills/roles/qa.test-automation.md +2 -0
  169. package/skills/roles/qa.web-ui.md +2 -0
  170. package/skills/roles/researcher.explorer.md +2 -0
  171. package/skills/roles/researcher.md +2 -0
  172. package/skills/roles/researcher.ux.md +2 -0
  173. package/skills/roles/reviewer.devil-advocate.md +2 -0
  174. package/skills/roles/reviewer.evaluator.md +2 -0
  175. package/skills/roles/reviewer.md +2 -0
  176. package/skills/roles/reviewer.trace.md +2 -0
  177. package/skills/roles/security.ai.md +2 -0
  178. package/skills/roles/security.appsec.md +2 -0
  179. package/skills/roles/security.cloud.md +2 -0
  180. package/skills/roles/security.legal-compliance.md +2 -0
  181. package/skills/roles/security.md +2 -0
  182. package/skills/roles/security.privacy.md +2 -0
  183. package/skills/roles/security.supply-chain.md +2 -0
  184. package/skills/security/blue-team.md +2 -0
  185. package/skills/security/code-audit.md +2 -0
  186. package/skills/security/pentest.md +2 -0
  187. package/skills/security/red-team.md +2 -0
  188. package/skills/security/threat-intel.md +2 -0
  189. package/skills/security/vuln-research.md +2 -0
  190. package/skills/strategy/competitive-landscape.md +2 -0
  191. package/skills/strategy/jobs-to-be-done.md +38 -0
  192. package/skills/strategy/market-research-methods.md +2 -0
  193. package/skills/strategy/narrative-arc.md +2 -0
  194. package/skills/strategy/pricing-positioning.md +2 -0
  195. package/skills/utility/clean-code.md +2 -0
  196. package/specialists/prompts/cx-engineer.md +1 -1
  197. package/specialists/registry.json +18 -9
  198. 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.sh) are
10
- * audited but NOT blocked. Editing them is normal development work on
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
- /(?:^|\/)agents\/registry\.json$/,
47
- /(?:^|\/)install\.sh$/,
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');
@@ -283,20 +283,26 @@ function parseWithDocs(value) {
283
283
  return LANE_ORDER;
284
284
  }
285
285
 
286
- const lanes = value.split(',').map(v => v.trim().toLowerCase());
287
- const validLanes = lanes.filter(lane => LANE_ORDER.includes(lane));
288
-
289
- // Map common aliases
290
- const mapped = validLanes.map(lane => {
291
- if (lane === 'adr') return 'adrs';
292
- if (lane === 'rfc') return 'rfcs';
293
- if (lane === 'runbook') return 'runbooks';
294
- if (lane === 'postmortem') return 'postmortems';
295
- if (lane === 'prd') return 'prds';
296
- return lane;
297
- });
298
-
299
- return [...new Set(mapped)]; // dedupe
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
- console.log('1. Review AGENTS.md');
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('2. Edit plan.md');
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('3. Use Intake');
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('4. Start working');
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 } = {}) {
@@ -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
@@ -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 search = await hybridSearchFn(rootDir, query, { limit: relatedLimit, env });
70
- related = (search?.results || []).map((hit) => ({
71
- path: hit.source_path || hit.id,
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
- * @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
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;