@geraldmaron/construct 1.0.3 → 1.0.5

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 (157) hide show
  1. package/README.md +51 -42
  2. package/agents/prompts/cx-ai-engineer.md +6 -26
  3. package/agents/prompts/cx-architect.md +1 -0
  4. package/agents/prompts/cx-business-strategist.md +2 -0
  5. package/agents/prompts/cx-data-analyst.md +6 -26
  6. package/agents/prompts/cx-docs-keeper.md +1 -31
  7. package/agents/prompts/cx-explorer.md +1 -0
  8. package/agents/prompts/cx-orchestrator.md +40 -112
  9. package/agents/prompts/cx-platform-engineer.md +2 -22
  10. package/agents/prompts/cx-product-manager.md +2 -1
  11. package/agents/prompts/cx-qa.md +0 -20
  12. package/agents/prompts/cx-rd-lead.md +2 -0
  13. package/agents/prompts/cx-researcher.md +77 -31
  14. package/agents/prompts/cx-security.md +11 -49
  15. package/agents/prompts/cx-sre.md +9 -43
  16. package/agents/prompts/cx-ux-researcher.md +1 -0
  17. package/agents/role-manifests.json +4 -4
  18. package/bin/construct +279 -4
  19. package/commands/understand/research.md +5 -3
  20. package/db/schema/004_recommendations.sql +46 -0
  21. package/db/schema/005_strategy.sql +21 -0
  22. package/lib/auto-docs.mjs +3 -4
  23. package/lib/beads-automation.mjs +16 -7
  24. package/lib/cli-commands.mjs +52 -2
  25. package/lib/comment-lint.mjs +7 -1
  26. package/lib/config/schema.mjs +3 -0
  27. package/lib/embed/conflict-detection.mjs +26 -9
  28. package/lib/embed/customer-profiles.mjs +37 -17
  29. package/lib/embed/daemon.mjs +10 -8
  30. package/lib/embed/recommendation-store.mjs +213 -14
  31. package/lib/embed/workspaces.mjs +53 -18
  32. package/lib/flavors/loader.mjs +136 -0
  33. package/lib/gates-audit.mjs +3 -3
  34. package/lib/health-check.mjs +1 -1
  35. package/lib/hooks/agent-tracker.mjs +22 -3
  36. package/lib/hooks/pre-compact.mjs +3 -0
  37. package/lib/hooks/pre-push-gate.mjs +14 -1
  38. package/lib/hooks/read-tracker.mjs +10 -101
  39. package/lib/hooks/session-optimize.mjs +3 -2
  40. package/lib/hooks/session-reflect.mjs +68 -0
  41. package/lib/host-capabilities.mjs +90 -1
  42. package/lib/init-unified.mjs +25 -2
  43. package/lib/init-update.mjs +246 -131
  44. package/lib/intake/classify.mjs +61 -183
  45. package/lib/intake/prepare.mjs +7 -0
  46. package/lib/intake/tables/creative.mjs +94 -0
  47. package/lib/intake/tables/operations.mjs +85 -0
  48. package/lib/intake/tables/research.mjs +85 -0
  49. package/lib/intake/tables/rnd.mjs +175 -0
  50. package/lib/intent-classifier.mjs +1 -0
  51. package/lib/knowledge/layout.mjs +10 -0
  52. package/lib/knowledge/research-store.mjs +109 -0
  53. package/lib/mcp/tools/telemetry.mjs +30 -78
  54. package/lib/model-router.mjs +61 -1
  55. package/lib/observation-store.mjs +19 -0
  56. package/lib/ollama-manager.mjs +1 -1
  57. package/lib/opencode-telemetry.mjs +4 -5
  58. package/lib/orchestration-policy.mjs +9 -0
  59. package/lib/outcomes/aggregate.mjs +104 -0
  60. package/lib/outcomes/record.mjs +115 -0
  61. package/lib/parity.mjs +121 -21
  62. package/lib/profiles/lifecycle.mjs +388 -0
  63. package/lib/profiles/loader.mjs +123 -0
  64. package/lib/profiles/validate-custom.mjs +114 -0
  65. package/lib/prompt-composer.js +106 -29
  66. package/lib/read-tracker-store.mjs +149 -0
  67. package/lib/reflect/extractor.mjs +193 -0
  68. package/lib/reflect.mjs +89 -2
  69. package/lib/sandbox.mjs +102 -0
  70. package/lib/server/index.mjs +76 -0
  71. package/lib/server/telemetry-login.mjs +17 -25
  72. package/lib/service-manager.mjs +30 -22
  73. package/lib/services/local-postgres.mjs +15 -0
  74. package/lib/services/telemetry-backend.mjs +1 -2
  75. package/lib/setup.mjs +8 -43
  76. package/lib/status.mjs +51 -5
  77. package/lib/storage/backend.mjs +12 -2
  78. package/lib/strategy-store.mjs +371 -0
  79. package/lib/telemetry/backends/local.mjs +6 -4
  80. package/lib/telemetry/client.mjs +185 -0
  81. package/lib/telemetry/ingest.mjs +13 -5
  82. package/lib/telemetry/team-rollup.mjs +9 -2
  83. package/lib/worker/trace.mjs +17 -27
  84. package/package.json +10 -2
  85. package/personas/construct.md +20 -20
  86. package/platforms/claude/settings.template.json +13 -0
  87. package/rules/common/research.md +44 -12
  88. package/scripts/sync-agents.mjs +11 -0
  89. package/skills/docs/backlog-proposal-workflow.md +2 -2
  90. package/skills/docs/customer-profile-workflow.md +1 -1
  91. package/skills/docs/evidence-ingest-workflow.md +5 -5
  92. package/skills/docs/prfaq-workflow.md +1 -1
  93. package/skills/docs/product-intelligence-review.md +1 -1
  94. package/skills/docs/product-intelligence-workflow.md +3 -3
  95. package/skills/docs/product-signal-workflow.md +48 -18
  96. package/skills/docs/research-workflow.md +26 -14
  97. package/skills/docs/strategy-workflow.md +36 -0
  98. package/skills/roles/architect.ai-systems.md +4 -2
  99. package/skills/roles/architect.data.md +4 -2
  100. package/skills/roles/architect.enterprise.md +4 -2
  101. package/skills/roles/architect.integration.md +4 -2
  102. package/skills/roles/architect.md +7 -5
  103. package/skills/roles/architect.platform.md +4 -2
  104. package/skills/roles/data-analyst.experiment.md +4 -2
  105. package/skills/roles/data-analyst.md +9 -7
  106. package/skills/roles/data-analyst.product-intelligence.md +5 -3
  107. package/skills/roles/data-analyst.product.md +4 -2
  108. package/skills/roles/data-analyst.telemetry.md +4 -2
  109. package/skills/roles/data-engineer.pipeline.md +4 -2
  110. package/skills/roles/data-engineer.vector-retrieval.md +4 -2
  111. package/skills/roles/data-engineer.warehouse.md +4 -2
  112. package/skills/roles/debugger.md +7 -5
  113. package/skills/roles/designer.accessibility.md +4 -2
  114. package/skills/roles/designer.md +10 -8
  115. package/skills/roles/engineer.ai.md +4 -2
  116. package/skills/roles/engineer.data.md +5 -3
  117. package/skills/roles/engineer.md +14 -12
  118. package/skills/roles/engineer.platform.md +5 -3
  119. package/skills/roles/operator.docs.md +6 -4
  120. package/skills/roles/operator.md +6 -4
  121. package/skills/roles/operator.release.md +4 -2
  122. package/skills/roles/operator.sre.md +5 -3
  123. package/skills/roles/orchestrator.md +5 -3
  124. package/skills/roles/product-manager.ai-product.md +4 -2
  125. package/skills/roles/product-manager.business-strategy.md +4 -2
  126. package/skills/roles/product-manager.enterprise.md +4 -2
  127. package/skills/roles/product-manager.growth.md +4 -2
  128. package/skills/roles/product-manager.md +6 -4
  129. package/skills/roles/product-manager.platform.md +4 -2
  130. package/skills/roles/product-manager.product.md +4 -2
  131. package/skills/roles/qa.ai-eval.md +4 -2
  132. package/skills/roles/qa.api-contract.md +4 -2
  133. package/skills/roles/qa.data-pipeline.md +4 -2
  134. package/skills/roles/qa.md +7 -5
  135. package/skills/roles/qa.test-automation.md +5 -3
  136. package/skills/roles/qa.web-ui.md +4 -2
  137. package/skills/roles/researcher.explorer.md +4 -2
  138. package/skills/roles/researcher.md +35 -20
  139. package/skills/roles/researcher.ux.md +4 -2
  140. package/skills/roles/reviewer.devil-advocate.md +4 -2
  141. package/skills/roles/reviewer.evaluator.md +4 -2
  142. package/skills/roles/reviewer.md +14 -12
  143. package/skills/roles/reviewer.trace.md +4 -2
  144. package/skills/roles/security.ai.md +4 -2
  145. package/skills/roles/security.appsec.md +4 -2
  146. package/skills/roles/security.cloud.md +4 -2
  147. package/skills/roles/security.legal-compliance.md +4 -2
  148. package/skills/roles/security.md +7 -5
  149. package/skills/roles/security.privacy.md +4 -2
  150. package/skills/roles/security.supply-chain.md +4 -2
  151. package/skills/routing.md +8 -1
  152. package/templates/docs/persona-artifact.md +36 -0
  153. package/templates/docs/research-brief.md +63 -9
  154. package/templates/docs/research-finding.md +26 -0
  155. package/templates/docs/skill-artifact.md +27 -0
  156. package/templates/docs/strategy.md +36 -0
  157. package/templates/homebrew/construct.rb +6 -6
@@ -0,0 +1,185 @@
1
+ /**
2
+ * lib/telemetry/client.mjs — shared local-first telemetry adapter.
3
+ *
4
+ * Local JSONL trace capture is the default. Remote export is opt-in through
5
+ * CONSTRUCT_TRACE_BACKEND and never throws into callers.
6
+ */
7
+ import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { randomUUID } from 'node:crypto';
11
+
12
+ import { createIngestClient } from './ingest.mjs';
13
+
14
+ const DEFAULT_MAX_BATCH = 50;
15
+
16
+ export const TRACE_BACKENDS = new Set(['local', 'langfuse', 'http', 'otel', 'none']);
17
+
18
+ function cleanUrl(value = '') {
19
+ return String(value || '').replace(/\/$/, '');
20
+ }
21
+
22
+ function inferRemoteBackend(env) {
23
+ const url = cleanUrl(env.CONSTRUCT_TELEMETRY_URL);
24
+ const hasKeys = Boolean(env.CONSTRUCT_TELEMETRY_PUBLIC_KEY && env.CONSTRUCT_TELEMETRY_SECRET_KEY);
25
+ if (!url) return 'local';
26
+ return hasKeys ? 'langfuse' : 'http';
27
+ }
28
+
29
+ export function resolveTraceBackend(env = process.env) {
30
+ const raw = String(env.CONSTRUCT_TRACE_BACKEND || '').trim().toLowerCase();
31
+ if (!raw) return inferRemoteBackend(env);
32
+ if (raw === 'remote' || raw === 'telemetry') return inferRemoteBackend(env);
33
+ if (raw === 'off') return 'none';
34
+ return TRACE_BACKENDS.has(raw) ? raw : 'local';
35
+ }
36
+
37
+ export function telemetryProviderLabel(env = process.env) {
38
+ return env.CONSTRUCT_TELEMETRY_PROVIDER || resolveTraceBackend(env);
39
+ }
40
+
41
+ function resolveProjectRoot({ rootDir, env = process.env } = {}) {
42
+ return rootDir || env.CX_TOOLKIT_DIR || env.PWD || process.cwd();
43
+ }
44
+
45
+ function localTraceEnabled(env = process.env) {
46
+ return env.CONSTRUCT_TRACE_LOCAL_ENABLED !== '0';
47
+ }
48
+
49
+ function traceShard() {
50
+ return new Date().toISOString().slice(0, 10);
51
+ }
52
+
53
+ function appendLocalTelemetry(type, body, { rootDir, env = process.env, onError = () => {} } = {}) {
54
+ if (!localTraceEnabled(env)) return;
55
+ const projectRoot = resolveProjectRoot({ rootDir, env });
56
+ const dir = join(projectRoot, '.cx', 'traces');
57
+ try {
58
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
59
+ appendFileSync(join(dir, `${traceShard()}.jsonl`), `${JSON.stringify({
60
+ traceId: body?.traceId || body?.id || randomUUID(),
61
+ spanId: body?.id || randomUUID(),
62
+ eventType: `telemetry.${type}`,
63
+ telemetryType: type,
64
+ project: body?.metadata?.project || body?.project || null,
65
+ role: body?.metadata?.agent || body?.metadata?.agentName || body?.name || null,
66
+ metadata: body?.metadata || {},
67
+ input: body?.input,
68
+ output: body?.output,
69
+ createdAt: body?.timestamp || body?.startTime || new Date().toISOString(),
70
+ })}\n`, 'utf8');
71
+ } catch (err) {
72
+ onError(err);
73
+ }
74
+ }
75
+
76
+ function buildOtlpPayload(batch) {
77
+ return {
78
+ resourceSpans: [{
79
+ resource: {
80
+ attributes: [
81
+ { key: 'service.name', value: { stringValue: 'construct' } },
82
+ ],
83
+ },
84
+ scopeSpans: [{
85
+ scope: { name: 'construct.telemetry' },
86
+ spans: batch.map((item) => {
87
+ const body = item.body || {};
88
+ const startedAt = Date.parse(body.startTime || body.timestamp || item.timestamp || new Date().toISOString());
89
+ const endedAt = Date.parse(body.endTime || body.startTime || body.timestamp || item.timestamp || new Date().toISOString());
90
+ return {
91
+ traceId: String(body.traceId || body.id || item.id).replace(/[^a-fA-F0-9]/g, '').padEnd(32, '0').slice(0, 32),
92
+ spanId: String(body.id || item.id).replace(/[^a-fA-F0-9]/g, '').padEnd(16, '0').slice(0, 16),
93
+ name: body.name || item.type,
94
+ kind: 1,
95
+ startTimeUnixNano: String(Math.max(0, startedAt) * 1_000_000),
96
+ endTimeUnixNano: String(Math.max(0, endedAt) * 1_000_000),
97
+ attributes: [
98
+ { key: 'construct.telemetry.type', value: { stringValue: item.type } },
99
+ ...(body.model ? [{ key: 'llm.model_name', value: { stringValue: String(body.model) } }] : []),
100
+ ],
101
+ };
102
+ }),
103
+ }],
104
+ }],
105
+ };
106
+ }
107
+
108
+ function createRemoteClient({ backend, env, fetchImpl, onError }) {
109
+ if (backend === 'local' || backend === 'none') return null;
110
+ if (backend === 'otel') {
111
+ const endpoint = cleanUrl(env.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT);
112
+ return createIngestClient({
113
+ baseUrl: endpoint,
114
+ endpointPath: endpoint.endsWith('/v1/traces') ? '' : '/v1/traces',
115
+ authMode: 'none',
116
+ payloadBuilder: buildOtlpPayload,
117
+ fetchImpl,
118
+ onError,
119
+ maxBatch: DEFAULT_MAX_BATCH,
120
+ });
121
+ }
122
+ if (backend === 'http') {
123
+ return createIngestClient({
124
+ baseUrl: cleanUrl(env.CONSTRUCT_TELEMETRY_URL),
125
+ endpointPath: '/ingest',
126
+ authMode: 'none',
127
+ fetchImpl,
128
+ onError,
129
+ maxBatch: DEFAULT_MAX_BATCH,
130
+ });
131
+ }
132
+ return createIngestClient({
133
+ baseUrl: cleanUrl(env.CONSTRUCT_TELEMETRY_URL),
134
+ publicKey: env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
135
+ secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
136
+ fetchImpl,
137
+ onError,
138
+ maxBatch: DEFAULT_MAX_BATCH,
139
+ });
140
+ }
141
+
142
+ export function createTelemetryClient({
143
+ env = process.env,
144
+ rootDir,
145
+ fetchImpl = globalThis.fetch,
146
+ onError = () => {},
147
+ localWrites = true,
148
+ } = {}) {
149
+ const backend = resolveTraceBackend(env);
150
+ const remote = createRemoteClient({ backend, env, fetchImpl, onError });
151
+ const localAvailable = localWrites && localTraceEnabled(env);
152
+ const remoteAvailable = Boolean(remote?.available);
153
+
154
+ function emit(type, body) {
155
+ if (!body) return;
156
+ if (localWrites) appendLocalTelemetry(type, body, { rootDir, env, onError });
157
+ if (!remoteAvailable) return;
158
+ try {
159
+ remote[type]?.(body);
160
+ } catch (err) {
161
+ onError(err);
162
+ }
163
+ }
164
+
165
+ return {
166
+ backend,
167
+ provider: telemetryProviderLabel(env),
168
+ available: localAvailable || remoteAvailable,
169
+ localAvailable,
170
+ remoteAvailable,
171
+ remoteStatus: remoteAvailable ? 'configured' : backend === 'local' || backend === 'none' ? backend : 'unconfigured',
172
+ trace: (body) => emit('trace', body),
173
+ traceUpdate: (body) => emit('traceUpdate', body),
174
+ generation: (body) => emit('generation', body),
175
+ generationUpdate: (body) => emit('generationUpdate', body),
176
+ span: (body) => emit('span', body),
177
+ spanUpdate: (body) => emit('spanUpdate', body),
178
+ event: (body) => emit('event', body),
179
+ score: (body) => emit('score', body),
180
+ async flush() {
181
+ if (remote?.flush) await remote.flush();
182
+ },
183
+ queueSize: () => remote?.queueSize?.() ?? 0,
184
+ };
185
+ }
@@ -35,17 +35,21 @@ export function createIngestClient({
35
35
  ).replace(/\/$/, ''),
36
36
  publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
37
37
  secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
38
+ endpointPath = '/api/public/ingestion',
39
+ authMode = 'basic',
40
+ payloadBuilder = (batch) => ({ batch }),
38
41
  flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
39
42
  maxBatch = DEFAULT_MAX_BATCH,
40
43
  onError = () => {},
41
44
  fetchImpl = globalThis.fetch,
42
45
  } = {}) {
43
- const available = Boolean(publicKey && secretKey && fetchImpl);
46
+ const requiresBasicAuth = authMode === 'basic';
47
+ const available = Boolean(baseUrl && fetchImpl && (!requiresBasicAuth || (publicKey && secretKey)));
44
48
  const queue = [];
45
49
  let flushTimer = null;
46
50
  let inflight = Promise.resolve();
47
51
 
48
- const authHeader = available
52
+ const authHeader = available && requiresBasicAuth
49
53
  ? `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`
50
54
  : '';
51
55
 
@@ -61,14 +65,17 @@ export function createIngestClient({
61
65
  async function flushBatch() {
62
66
  if (!available || queue.length === 0) return;
63
67
  const batch = queue.splice(0, maxBatch);
64
- const payload = JSON.stringify({ batch });
68
+ const payload = JSON.stringify(payloadBuilder(batch));
65
69
  const prev = inflight;
66
70
  inflight = (async () => {
67
71
  try { await prev; } catch { /* ignore prior */ }
68
72
  try {
69
- const res = await fetchImpl(`${baseUrl}/api/public/ingestion`, {
73
+ const res = await fetchImpl(`${baseUrl}${endpointPath}`, {
70
74
  method: 'POST',
71
- headers: { Authorization: authHeader, 'Content-Type': 'application/json' },
75
+ headers: {
76
+ ...(authHeader ? { Authorization: authHeader } : {}),
77
+ 'Content-Type': 'application/json',
78
+ },
72
79
  body: payload,
73
80
  });
74
81
  if (!res.ok && res.status !== 207) {
@@ -104,6 +111,7 @@ export function createIngestClient({
104
111
  span: (body) => push('span-create', body),
105
112
  spanUpdate: (body) => push('span-update', body),
106
113
  event: (body) => push('event-create', body),
114
+ score: (body) => push('score-create', body),
107
115
  async flush() {
108
116
  if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
109
117
  while (queue.length > 0) await flushBatch();
@@ -3,22 +3,29 @@
3
3
  *
4
4
  * Groups agent traces by teamId (stamped by construct headhunt at overlay creation),
5
5
  * computes per-team success rates, latency, handoff counts, and common blockers.
6
- * Backend is selected via CONSTRUCT_TRACE_BACKEND env var (default: remote).
6
+ * Backend is selected via CONSTRUCT_TRACE_BACKEND env var (default: local).
7
7
  * Called by 'construct team review'.
8
8
  */
9
9
 
10
10
  import * as remoteBackend from './backends/remote.mjs';
11
+ import * as localBackend from './backends/local.mjs';
11
12
  import * as noopBackend from './backends/noop.mjs';
13
+ import { resolveTraceBackend } from './client.mjs';
12
14
 
13
15
  const BACKENDS = {
16
+ local: localBackend,
17
+ langfuse: remoteBackend,
18
+ http: remoteBackend,
19
+ otel: noopBackend,
14
20
  remote: remoteBackend,
21
+ none: noopBackend,
15
22
  noop: noopBackend,
16
23
  };
17
24
 
18
25
  const DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
19
26
 
20
27
  function resolveBackend() {
21
- const name = process.env.CONSTRUCT_TRACE_BACKEND ?? 'remote';
28
+ const name = resolveTraceBackend(process.env);
22
29
  const backend = BACKENDS[name];
23
30
  if (!backend) throw new Error(`Unknown trace backend: ${name}. Available: ${Object.keys(BACKENDS).join(', ')}`);
24
31
  return backend;
@@ -7,16 +7,11 @@
7
7
  *
8
8
  * 1. `.cx/traces/<YYYY-MM-DD>.jsonl` — append-only local JSONL. Always on,
9
9
  * no credentials required. Tail live, archive, or replay.
10
- * 2. Remote telemetry backendfire-and-forget batched ingest when
11
- * CONSTRUCT_TELEMETRY_PUBLIC_KEY + CONSTRUCT_TELEMETRY_SECRET_KEY are
12
- * configured and CONSTRUCT_TRACE_BACKEND is `remote` (or unset).
13
- * Each unique traceId triggers one trace-create plus an event-create
14
- * per emitted event. Failures never throw — observability must not
10
+ * 2. Optional remote exportselected by CONSTRUCT_TRACE_BACKEND. Each
11
+ * unique traceId triggers one trace-create plus an event-create per
12
+ * emitted event. Failures never throw because observability must not
15
13
  * break the caller.
16
14
  *
17
- * Reads CONSTRUCT_TELEMETRY_PUBLIC_KEY / CONSTRUCT_TELEMETRY_SECRET_KEY /
18
- * CONSTRUCT_TELEMETRY_URL from env.
19
- *
20
15
  * Event types in current use:
21
16
  * intake.received — daemon ingested a file
22
17
  * intake.triaged — classifyRdIntake produced a triage block
@@ -35,7 +30,7 @@ import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
35
30
  import { randomBytes } from 'node:crypto';
36
31
  import path from 'node:path';
37
32
 
38
- import { createIngestClient } from '../telemetry/ingest.mjs';
33
+ import { createTelemetryClient } from '../telemetry/client.mjs';
39
34
 
40
35
  const TRACE_SUBDIR = '.cx/traces';
41
36
 
@@ -73,23 +68,18 @@ export function newSpanId() {
73
68
  return `span-${randomBytes(6).toString('hex')}`;
74
69
  }
75
70
 
76
- // Reused per-process. The ingest client batches; recreating it would lose
77
- // the queue. `available` flips false when keys aren't set, which is the
78
- // graceful no-op path for solo-mode users without no telemetry backend configured.
71
+ // Reused per-process. The adapter batches remote export; recreating it would
72
+ // lose the queue. Local JSONL is already written by emitTraceEvent, so this
73
+ // client disables its own local writer.
79
74
  let telemetryClient = null;
80
75
  const seenTraces = new Set();
81
76
 
82
- function getTelemetryClient(env = process.env) {
77
+ function getTelemetryClient(env = process.env, rootDir) {
83
78
  if (telemetryClient !== null) return telemetryClient;
84
- const backend = (env.CONSTRUCT_TRACE_BACKEND || 'remote').toLowerCase();
85
- if (backend === 'none' || backend === 'off' || backend === 'local') {
86
- telemetryClient = { available: false, trace: () => {}, event: () => {} };
87
- return telemetryClient;
88
- }
89
- telemetryClient = createIngestClient({
90
- baseUrl: (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, ''),
91
- publicKey: env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
92
- secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
79
+ telemetryClient = createTelemetryClient({
80
+ env,
81
+ rootDir,
82
+ localWrites: false,
93
83
  });
94
84
  return telemetryClient;
95
85
  }
@@ -103,9 +93,9 @@ export function _resetTelemetryClient() {
103
93
  }
104
94
 
105
95
 
106
- function exportToRemote(event, env) {
107
- const client = getTelemetryClient(env);
108
- if (!client.available) return;
96
+ function exportToRemote(event, env, rootDir) {
97
+ const client = getTelemetryClient(env, rootDir);
98
+ if (!client.remoteAvailable) return;
109
99
 
110
100
  // First time we see a traceId, register the trace so subsequent events
111
101
  // attach to a known parent.
@@ -176,7 +166,7 @@ export function emitTraceEvent({
176
166
  };
177
167
  appendFileSync(path.join(dir, `${todayShard()}.jsonl`), `${JSON.stringify(event)}\n`, 'utf8');
178
168
 
179
- // Fire-and-forget export to remote telemetry backend. Silent no-op when not configured.
180
- exportToRemote(event, env);
169
+ // Fire-and-forget export to the configured remote adapter. Silent no-op when not configured.
170
+ exportToRemote(event, env, rootDir);
181
171
  return event;
182
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {
@@ -42,15 +42,22 @@
42
42
  "scripts": {
43
43
  "mcp": "node lib/mcp/server.mjs",
44
44
  "test": "node scripts/run-tests.mjs",
45
+ "test:functional": "node --test --test-timeout=30000 tests/functional/*.functional.test.mjs",
46
+ "test:unit": "node scripts/run-tests.mjs --exclude=tests/functional",
45
47
  "docs:init": "node lib/init-docs.mjs --yes",
46
48
  "docs:update": "node ./bin/construct docs:update",
47
49
  "docs:site": "node ./bin/construct docs:site",
48
50
  "docs:dev": "npm --prefix apps/docs run dev",
49
51
  "docs:build": "npm --prefix apps/docs run build",
50
52
  "lint:comments": "node ./bin/construct lint:comments",
53
+ "lint:prose": "node scripts/lint-prose.mjs",
54
+ "lint:profiles": "node scripts/lint-profiles.mjs",
55
+ "learning:status": "node scripts/learning-status.mjs",
51
56
  "lint:templates": "node scripts/lint-commits-pr.mjs",
52
57
  "eval:routing": "node scripts/eval/score-intent-classifier.mjs",
53
58
  "release:check": "node ./bin/construct doctor && npm test && node ./bin/construct docs:update --check && node ./bin/construct dashboard:sync --check && node ./bin/construct lint:comments && node scripts/lint-commits-pr.mjs",
59
+ "release:preflight": "node scripts/pre-release-check.mjs",
60
+ "release:preflight:no-auth": "node scripts/pre-release-check.mjs --skip-auth",
54
61
  "npm:publish": "npm run release:check && npm publish --access public",
55
62
  "postinstall": "node ./bin/construct-postinstall.mjs"
56
63
  },
@@ -61,6 +68,7 @@
61
68
  "postgres": "^3.4.9"
62
69
  },
63
70
  "overrides": {
64
- "express-rate-limit": "8.5.1"
71
+ "express-rate-limit": "8.5.1",
72
+ "protobufjs": "^8.4.2"
65
73
  }
66
74
  }
@@ -1,24 +1,24 @@
1
1
  <!--
2
- personas/construct.md Construct persona prompt.
2
+ personas/construct.md. Construct persona prompt.
3
3
 
4
4
  Defines the single user-facing AI interface and its session-start behavior,
5
5
  routing rules, approval boundaries, and output contract. Loaded by sync-agents
6
6
  and emitted to every supported platform.
7
7
  -->
8
- You are Construct the single AI interface for everything from a quick question to a full software lifecycle. The user talks only to you; internal routing and specialist dispatch are implementation detail.
8
+ You are Construct. the single AI interface for everything from a quick question to a full software lifecycle. The user talks only to you; internal routing and specialist dispatch are implementation detail.
9
9
 
10
10
  ## Start of every session
11
11
 
12
- Before responding, run in parallel do not narrate:
13
- 1. `project_context` state from `.cx/context.md`
14
- 2. `memory_search` with the basename of CWD prior session context and user preferences
12
+ Before responding, run in parallel. do not narrate:
13
+ 1. `project_context`. state from `.cx/context.md`
14
+ 2. `memory_search` with the basename of CWD. prior session context and user preferences
15
15
  3. Read `AGENTS.md`, `plan.md`, and the relevant docs for the current task when present
16
- 4. Check `.cx/handoffs/` for the most recent handoff if another session was active, read it to understand what was in progress and what NOT to touch
16
+ 4. Check `.cx/handoffs/` for the most recent handoff. if another session was active, read it to understand what was in progress and what NOT to touch
17
17
 
18
18
  Apply results silently. If memory returns preferences or past decisions, honor them without asking the user to repeat.
19
19
 
20
20
  Honor the project operating hierarchy:
21
- - Beads (`bd`) is the durable source of truth for tasks; hygiene contract claim, close, supersede, prune lives in `rules/common/beads-hygiene.md`. `bd ready` for unblocked work, `bd show <id>` for the active issue.
21
+ - Beads (`bd`) is the durable source of truth for tasks; hygiene contract. claim, close, supersede, prune. lives in `rules/common/beads-hygiene.md`. `bd ready` for unblocked work, `bd show <id>` for the active issue.
22
22
  - `plan.md` is the human-readable implementation plan
23
23
  - cass-memory via MCP `memory` is for cross-tool/session recall, not task tracking
24
24
 
@@ -26,12 +26,12 @@ Use the single-writer rule whenever multiple sessions are active: if two session
26
26
 
27
27
  ## Classify before acting
28
28
 
29
- Use the code-backed orchestration policy for intent, execution track, specialist selection, escalation, and approval boundaries. Visual deliverables (wireframes, diagrams, decks) are first-class use real visual tools, not bullet prose.
29
+ Use the code-backed orchestration policy for intent, execution track, specialist selection, escalation, and approval boundaries. Visual deliverables (wireframes, diagrams, decks) are first-class. use real visual tools, not bullet prose.
30
30
 
31
31
  Execution model:
32
- - **Immediate** answer or act directly when no hidden worker is needed.
33
- - **Focused** dispatch one bounded specialist path; return in Construct's voice.
34
- - **Orchestrated** plan → challenge → build → validate, with tracker-backed slices and explicit file ownership.
32
+ - **Immediate**. answer or act directly when no hidden worker is needed.
33
+ - **Focused**. dispatch one bounded specialist path; return in Construct's voice.
34
+ - **Orchestrated**. plan → challenge → build → validate, with tracker-backed slices and explicit file ownership.
35
35
 
36
36
  Devil's advocate is mandatory for new architectural directions, AI/agent workflow changes, security or data-integrity changes, and promoting a temporary capability to persistent.
37
37
 
@@ -39,9 +39,9 @@ Devil's advocate is mandatory for new architectural directions, AI/agent workflo
39
39
 
40
40
  `routeRequest` returns three artifacts; honor all three:
41
41
 
42
- 1. **Gates** `framingChallenge`, `externalResearch`, `docAuthoring`
43
- 2. **Contract chain** typed handoffs from `agents/contracts.json`. Call `agent_contract` MCP tool at handoff.
44
- 3. **Specialist sequence** dispatch plan with ordering/parallel markers.
42
+ 1. **Gates**. `framingChallenge`, `externalResearch`, `docAuthoring`
43
+ 2. **Contract chain**. typed handoffs from `agents/contracts.json`. Call `agent_contract` MCP tool at handoff.
44
+ 3. **Specialist sequence**. dispatch plan with ordering/parallel markers.
45
45
 
46
46
  Before DONE: postconditions met · sources cited · framing logged · ADRs have Rejected alternatives.
47
47
 
@@ -50,9 +50,9 @@ Before DONE: postconditions met · sources cited · framing logged · ADRs have
50
50
  - **Working branch is surfaced every session** at the top of session-start. Restate it before any mutating operation.
51
51
  - **Never commit, push, or merge without asking first.** Before `git commit`, `git push`, or `gh pr merge`: state branch, show the proposed message / refspec / PR number verbatim, wait for explicit yes. A batch go-ahead covers a defined sequence; new commits later are their own gate. See `rules/common/commit-approval.md`.
52
52
 
53
- ## R&D intake surface
53
+ ## Intake surface
54
54
 
55
- Session-start surfaces pending intake at `.cx/intake/pending/<id>.json`. Read with `construct intake show <id>`; the triage block names the primary owner, recommended chain, and next action. For non-trivial signals, plan with `construct graph from-intake <id>` and update node status with evidence (`construct graph status … done --evidence=…`). A node cannot reach `done` without an evidence record. Team / enterprise mode wraps tool calls in the MCP broker when it returns `ApprovalRequired`, surface the question; never bypass.
55
+ The active profile (`construct profile show`) sets the intake taxonomy. Session-start surfaces pending intake at `.cx/intake/pending/<id>.json`. Read with `construct intake show <id>`; the triage block names the primary owner, recommended chain, and next action. For non-trivial signals, plan with `construct graph from-intake <id>` and update node status with evidence (`construct graph status … done --evidence=…`). A node cannot reach `done` without an evidence record. Team / enterprise mode wraps tool calls in the MCP broker; when it returns `ApprovalRequired`, surface the question and never bypass.
56
56
 
57
57
  ## Action discipline
58
58
 
@@ -73,15 +73,15 @@ Load-bearing state: `AGENTS.md`, `.cx/context.md`/`.json`, `docs/README.md`, `do
73
73
  ## Quality gates
74
74
 
75
75
  After any implementation, dispatch validation before marking done:
76
- 1. cx-reviewer correctness, regression, coverage
77
- 2. cx-qa tests pass, coverage meets threshold
76
+ 1. cx-reviewer. correctness, regression, coverage
77
+ 2. cx-qa. tests pass, coverage meets threshold
78
78
  3. cx-security if auth/secrets/user data touched
79
79
 
80
80
  Do not mark `done` until cx-reviewer and cx-qa return verdicts. BLOCKED or any CRITICAL finding stops shipping.
81
81
 
82
82
  ## Hard release gates
83
83
 
84
- Run `npm run release:check` before any commit or push never wait for CI. Commits follow `.gitmessage`; PRs follow `.github/pull_request_template.md`. Full policy: `rules/common/release-gates.md`.
84
+ Run `npm run release:check` before any commit or push. never wait for CI. Commits follow `.gitmessage`; PRs follow `.github/pull_request_template.md`. Full policy: `rules/common/release-gates.md`.
85
85
 
86
86
  ## Loop guard
87
87
 
@@ -91,6 +91,6 @@ Before stopping: surface incomplete tracker-linked plan slices and unmet accepta
91
91
 
92
92
  ## Drive mode
93
93
 
94
- Activates on word-boundary triggers `/work:drive`, standalone `drive`, or `full send`. Substring matches do not count.
94
+ Activates on word-boundary triggers. `/work:drive`, standalone `drive`, or `full send`. Substring matches do not count.
95
95
 
96
96
  On trigger: orchestrated track, skip planning confirmation, continue until verification or a real blocker. State the dispatch plan upfront; brief status at phase transitions. User sees plan and outcomes, not deliberation.
@@ -460,6 +460,19 @@
460
460
  ],
461
461
  "id": "stop:optimize-agents",
462
462
  "description": "Check performance reviews and auto-optimize low-scoring agents at session end"
463
+ },
464
+ {
465
+ "matcher": "*",
466
+ "hooks": [
467
+ {
468
+ "type": "command",
469
+ "command": "node \"$HOME/.construct/lib/hooks/session-reflect.mjs\"",
470
+ "timeout": 5,
471
+ "async": true
472
+ }
473
+ ],
474
+ "id": "stop:auto-reflect",
475
+ "description": "Deterministic session-end reflect: extracts a transcript summary into .cx/observations (500ms hard budget; CONSTRUCT_REFLECT_AUTO=off to disable)"
463
476
  }
464
477
  ],
465
478
  "UserPromptSubmit": [
@@ -10,7 +10,31 @@ that depends on external facts or evolving internal evidence.
10
10
 
11
11
  Construct treats research as a reproducible evidence-gathering process, not free-form browsing. If a claim could change decisions, scope, architecture, or roadmap, it must be tied to verifiable evidence.
12
12
 
13
- ## 1. Start order
13
+ ## 1. Recency discipline
14
+
15
+ Research always starts from the most recent available evidence.
16
+
17
+ - Default to sources from the current year before earlier years — always search most-recent-first.
18
+ - For fast-moving topics (AI tools, LLM behavior, cloud APIs, security advisories, market data), treat anything older than 12 months as presumptively stale unless a newer source confirms it is still accurate.
19
+ - When using a search engine or index, always filter or sort by date — do not rely on relevance ranking alone.
20
+ - State the publication or access date for every external source. If a source has no date, treat its confidence as `low` until recency is established another way.
21
+
22
+ ## 2. Domain-specific starting points
23
+
24
+ Use the narrowest, most authoritative starting point for the research domain:
25
+
26
+ | Domain | Starting points (most recent first) |
27
+ |---|---|
28
+ | AI tools, LLM behavior, multi-agent | arXiv (cs.AI, cs.SE, cs.CL), ACL Anthology, NeurIPS/ICML/ICLR proceedings, then vendor research blogs |
29
+ | Developer tools, IDE, editor | Stack Overflow Developer Survey (current year), JetBrains Developer Survey, GitHub/Copilot blog, then product changelogs |
30
+ | Security, CVEs, supply chain | NVD, GitHub Security Advisories, OWASP, vendor security blogs (Google Project Zero, Microsoft Security Response), then ProjectDiscovery/Snyk reports |
31
+ | Market data, adoption, ARR | Primary company announcements or SEC filings, then TechCrunch/Bloomberg/WSJ (where citing company sources), then analyst reports |
32
+ | Cloud infra, APIs, SDKs | Official vendor docs for the exact version, changelog, migration guides |
33
+ | Regulatory, compliance, privacy | Primary regulation text, then official guidance from the issuing authority, then law firm analysis |
34
+
35
+ Tertiary sources (blogs, forums, Q&A, AI-generated summaries) may help locate primaries. They are not sufficient evidence for load-bearing claims.
36
+
37
+ ## 3. Start order
14
38
 
15
39
  Start with the narrowest authoritative source that can answer the question:
16
40
 
@@ -28,9 +52,7 @@ Start with the narrowest authoritative source that can answer the question:
28
52
  4. **Tertiary sources last**
29
53
  - blogs, forums, Q&A, analyst summaries, AI-generated summaries
30
54
 
31
- Tertiary sources may help discover primary sources. They are not sufficient evidence for load-bearing claims.
32
-
33
- ## 2. Required metadata for every source
55
+ ## 4. Required metadata for every source
34
56
 
35
57
  Record:
36
58
 
@@ -40,9 +62,16 @@ Record:
40
62
  - publication date, release date, or access date
41
63
  - why this source is relevant
42
64
 
43
- If a source has no date and the topic is time-sensitive, treat confidence as reduced until recency is established another way.
65
+ ## 5. URL verification
66
+
67
+ Every URL cited in a committed document must be verified before the document is published.
68
+
69
+ - Fetch the URL. Confirm it resolves and the content matches the cited claim.
70
+ - Do not cite aggregate or index pages (e.g., arxiv.org search results, Google Scholar listings) for quantitative claims — cite the specific paper or article URL.
71
+ - If a URL returns a 404, paywall, or redirect loop, find the canonical source or replace the citation.
72
+ - Unverified URLs must be marked `[unverified]` until confirmed.
44
73
 
45
- ## 3. Verification rules
74
+ ## 6. Verification rules
46
75
 
47
76
  For each load-bearing claim:
48
77
 
@@ -54,7 +83,7 @@ For each load-bearing claim:
54
83
 
55
84
  Claims about versions, APIs, security, pricing, compatibility, regulations, and timelines must cite the exact version/date basis.
56
85
 
57
- ## 4. Reproducibility
86
+ ## 7. Reproducibility
58
87
 
59
88
  Research must be reproducible by another person in the repo.
60
89
 
@@ -67,7 +96,7 @@ Record:
67
96
 
68
97
  If you cannot explain how the answer was obtained, the research is incomplete.
69
98
 
70
- ## 5. Evidence thresholds
99
+ ## 8. Evidence thresholds
71
100
 
72
101
  Recommendations must state what evidence threshold was used.
73
102
 
@@ -81,27 +110,30 @@ Examples:
81
110
 
82
111
  If the threshold is not met, the output should recommend more research, a weaker artifact, or a narrower decision.
83
112
 
84
- ## 6. Output standard
113
+ ## 9. Output standard
85
114
 
86
115
  Research outputs should include:
87
116
 
88
117
  - question
89
118
  - method
90
- - sources
119
+ - sources (with dates and classes)
91
120
  - findings
92
121
  - confidence
93
122
  - open questions
94
123
  - recommendation or next step
95
124
 
96
- Every substantive finding should point to a source path, URL, or document reference.
125
+ Every substantive finding should point to a verified source path, URL, or document reference.
97
126
 
98
- ## 7. Anti-patterns
127
+ ## 10. Anti-patterns
99
128
 
100
129
  Do not:
101
130
 
131
+ - start research from older years when more-recent sources are available
102
132
  - stop at the first plausible answer
103
133
  - cite a blog when the spec or source code is available
134
+ - cite an aggregate or index page when the specific document is available
104
135
  - present inference as if the source said it directly
105
136
  - ignore conflicting evidence
106
137
  - use stale undated material for fast-moving topics without saying so
107
138
  - promote weak product evidence into committed requirements
139
+ - include URLs that have not been fetched and confirmed
@@ -38,6 +38,7 @@ import {
38
38
  } from "../lib/mcp-platform-config.mjs";
39
39
  import { loadConstructEnv } from "../lib/env-config.mjs";
40
40
  import { inlineRoleAntiPatterns, PROMPT_WORD_CAP } from "../lib/role-preload.mjs";
41
+ import { resolveActiveProfile } from "../lib/profiles/loader.mjs";
41
42
  import { resolveTiersForPrimary } from "../lib/model-router.mjs";
42
43
  import { stampFrontmatter } from "../lib/doc-stamp.mjs";
43
44
 
@@ -499,7 +500,17 @@ function buildEntries() {
499
500
  });
500
501
  }
501
502
 
503
+ // Filter agents by the active profile's role set. RND lists every specialist
504
+ // so behavior is unchanged for the default; non-RND profiles emit only the
505
+ // specialists they declare. Profiles whose role list is empty (legacy/test)
506
+ // fall through to no filter so callers don't get a silent empty registry.
507
+ const activeProfile = resolveActiveProfile(process.cwd());
508
+ const profileRoles = Array.isArray(activeProfile?.roles) && activeProfile.roles.length > 0
509
+ ? new Set(activeProfile.roles)
510
+ : null;
511
+
502
512
  for (const agent of registry.agents ?? []) {
513
+ if (profileRoles && !profileRoles.has(agent.name)) continue;
503
514
  entries.push({
504
515
  ...agent,
505
516
  isPersona: false,