@plainconceptsplatform/loop-task 2.6.0 → 2.8.0

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.
@@ -11,6 +11,10 @@ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
11
11
  import { OTLPMetricExporter as OTLPMetricExporterGrpc } from "@opentelemetry/exporter-metrics-otlp-grpc";
12
12
  import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
13
13
  import { trace, context, propagation, metrics, SpanStatusCode, SpanKind } from "@opentelemetry/api";
14
+ import { logs } from "@opentelemetry/api-logs";
15
+ import { LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
16
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
17
+ import { OTLPLogExporter as OTLPLogExporterGrpc } from "@opentelemetry/exporter-logs-otlp-grpc";
14
18
  import { daemonLog } from "../daemon-log.js";
15
19
  class OtelSpan {
16
20
  constructor(span, ctx, onEnd) {
@@ -79,9 +83,11 @@ export class OpenTelemetryAdapter {
79
83
  constructor(settings) {
80
84
  this.sdk = null;
81
85
  this.tracerProvider = null;
86
+ this.loggerProvider = null;
82
87
  this.settings = settings;
83
88
  this.tracer = trace.getTracer("loop-task");
84
89
  this.meter = metrics.getMeter("loop-task");
90
+ this.logger = logs.getLogger("loop-task");
85
91
  this.status = {
86
92
  enabled: settings.telemetryEnabled,
87
93
  exporterConfigured: !!settings.telemetryEndpoint,
@@ -93,7 +99,12 @@ export class OpenTelemetryAdapter {
93
99
  captureCommandOutput: settings.telemetryCaptureCommandOutput,
94
100
  exporterState: this.resolveExporterState(settings),
95
101
  };
96
- // Initialize metric instruments (always created no-op when no SDK)
102
+ if (settings.telemetryEnabled && settings.telemetryEndpoint) {
103
+ this.initializeSdk(settings);
104
+ }
105
+ // Initialize metric instruments AFTER SDK startup so they bind to the
106
+ // real MeterProvider, not the default no-op one. When the SDK is not
107
+ // initialized, these still work — they bind to the API-level no-op meter.
97
108
  this.runCounter = this.meter.createCounter(METRIC_NAMES.RUNS);
98
109
  this.runDurationHistogram = this.meter.createHistogram(METRIC_NAMES.RUN_DURATION);
99
110
  this.taskCounter = this.meter.createCounter(METRIC_NAMES.TASKS);
@@ -109,9 +120,6 @@ export class OpenTelemetryAdapter {
109
120
  this.agentCacheWriteTokensCounter = this.meter.createCounter(METRIC_NAMES.AGENT_CACHE_WRITE_TOKENS);
110
121
  this.agentCostCounter = this.meter.createCounter(METRIC_NAMES.AGENT_COST);
111
122
  this.failureCounter = this.meter.createCounter(METRIC_NAMES.FAILURES);
112
- if (settings.telemetryEnabled && settings.telemetryEndpoint) {
113
- this.initializeSdk(settings);
114
- }
115
123
  }
116
124
  resolveExporterState(settings) {
117
125
  if (!settings.telemetryEnabled)
@@ -162,7 +170,24 @@ export class OpenTelemetryAdapter {
162
170
  instrumentations: [],
163
171
  });
164
172
  this.sdk.start();
173
+ // Re-obtain tracer and meter from the global API after SDK startup.
174
+ // The API now resolves to the real providers registered by NodeSDK.
165
175
  this.tracer = trace.getTracer(settings.telemetryServiceName);
176
+ this.meter = metrics.getMeter(settings.telemetryServiceName);
177
+ // Initialize OTLP log exporter
178
+ const logsUrl = endpoint.endsWith("/v1/logs")
179
+ ? endpoint
180
+ : `${endpoint}/v1/logs`;
181
+ const logExporter = isGrpc
182
+ ? new OTLPLogExporterGrpc({ url: settings.telemetryEndpoint })
183
+ : new OTLPLogExporter({ url: logsUrl, headers });
184
+ const loggerProvider = new LoggerProvider({
185
+ resource,
186
+ processors: [new SimpleLogRecordProcessor({ exporter: logExporter })],
187
+ });
188
+ logs.setGlobalLoggerProvider(loggerProvider);
189
+ this.loggerProvider = loggerProvider;
190
+ this.logger = logs.getLogger(settings.telemetryServiceName);
166
191
  this.status.exporterState = "configured";
167
192
  daemonLog(`telemetry: SDK initialized, endpoint=${settings.telemetryEndpoint}`);
168
193
  }
@@ -197,6 +222,24 @@ export class OpenTelemetryAdapter {
197
222
  return "http/protobuf";
198
223
  return this.settings.telemetryProtocol;
199
224
  }
225
+ /**
226
+ * Check if any agent serve sidecar is alive.
227
+ * When serve is alive, static OTEL config lives in the serve process
228
+ * and we skip per-task injection.
229
+ */
230
+ checkServeAlive() {
231
+ try {
232
+ const integrations = getAgentIntegrations();
233
+ for (const integration of integrations) {
234
+ if (integration.isServeAlive?.())
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ }
200
243
  startLoop(input) {
201
244
  const span = this.tracer.startSpan(SPAN_NAMES.LOOP_RUN, { kind: SpanKind.SERVER });
202
245
  span.setAttributes({
@@ -324,6 +367,20 @@ export class OpenTelemetryAdapter {
324
367
  activeSpan.setAttribute("gen_ai.request.model", input.model);
325
368
  }
326
369
  }
370
+ logEvent(level, message, attributes) {
371
+ try {
372
+ const record = this.logger.emit({
373
+ body: message,
374
+ severityText: level,
375
+ attributes: attributes ?? {},
376
+ });
377
+ // Ensure the log record is flushed in a timely manner
378
+ void record;
379
+ }
380
+ catch (err) {
381
+ daemonLog(`telemetry: log emit failed: ${String(err)}`);
382
+ }
383
+ }
327
384
  prepareChildProcess(invocation, childContext, integrationOverride) {
328
385
  const env = {};
329
386
  const endpoint = this.resolveEndpoint();
@@ -337,25 +394,34 @@ export class OpenTelemetryAdapter {
337
394
  env.TRACESTATE = childContext.traceState;
338
395
  }
339
396
  }
340
- env.OTEL_EXPORTER_OTLP_ENDPOINT = endpoint;
341
- env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = endpoint;
342
- env.OTEL_EXPORTER_OTLP_PROTOCOL = protocol;
343
- env.OTEL_TRACES_EXPORTER = "otlp";
344
- const authHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
345
- ?? process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS;
346
- if (authHeaders) {
347
- env.OTEL_EXPORTER_OTLP_HEADERS = authHeaders;
397
+ // Check if an agent serve sidecar is alive — if so, static OTEL config
398
+ // lives in the serve process env and we skip per-task injection.
399
+ const serveAlive = this.checkServeAlive();
400
+ if (!serveAlive) {
401
+ env.OTEL_EXPORTER_OTLP_ENDPOINT = endpoint;
402
+ env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = endpoint;
403
+ env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = endpoint;
404
+ env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = endpoint;
405
+ env.OTEL_EXPORTER_OTLP_PROTOCOL = protocol;
406
+ env.OTEL_TRACES_EXPORTER = "otlp";
407
+ env.OTEL_METRICS_EXPORTER = "otlp";
408
+ env.OTEL_LOGS_EXPORTER = "otlp";
409
+ const authHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
410
+ ?? process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS;
411
+ if (authHeaders) {
412
+ env.OTEL_EXPORTER_OTLP_HEADERS = authHeaders;
413
+ }
414
+ const correlationAttrs = {
415
+ [CORRELATION_KEYS.RUN_ID]: childContext.runId,
416
+ [CORRELATION_KEYS.LOOP_ID]: childContext.loopId,
417
+ };
418
+ if (childContext.taskId)
419
+ correlationAttrs[CORRELATION_KEYS.TASK_ID] = childContext.taskId;
420
+ if (childContext.projectId)
421
+ correlationAttrs[CORRELATION_KEYS.PROJECT_ID] = childContext.projectId;
422
+ const mergedResourceAttrs = this.mergeResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES, correlationAttrs);
423
+ env.OTEL_RESOURCE_ATTRIBUTES = mergedResourceAttrs;
348
424
  }
349
- const correlationAttrs = {
350
- [CORRELATION_KEYS.RUN_ID]: childContext.runId,
351
- [CORRELATION_KEYS.LOOP_ID]: childContext.loopId,
352
- };
353
- if (childContext.taskId)
354
- correlationAttrs[CORRELATION_KEYS.TASK_ID] = childContext.taskId;
355
- if (childContext.projectId)
356
- correlationAttrs[CORRELATION_KEYS.PROJECT_ID] = childContext.projectId;
357
- const mergedResourceAttrs = this.mergeResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES, correlationAttrs);
358
- env.OTEL_RESOURCE_ATTRIBUTES = mergedResourceAttrs;
359
425
  // Apply agent-specific integrations when auto-instrumentation is enabled
360
426
  let integrationId;
361
427
  if (this.settings.telemetryAutoInstrumentAgents || integrationOverride) {
@@ -419,6 +485,9 @@ export class OpenTelemetryAdapter {
419
485
  if (this.tracerProvider) {
420
486
  flushes.push(this.tracerProvider.forceFlush());
421
487
  }
488
+ if (this.loggerProvider) {
489
+ flushes.push(this.loggerProvider.forceFlush());
490
+ }
422
491
  const sdkInternals = this.sdk;
423
492
  if (typeof sdkInternals._meterProvider?.forceFlush === "function") {
424
493
  flushes.push(sdkInternals._meterProvider.forceFlush());
@@ -450,6 +519,16 @@ export class OpenTelemetryAdapter {
450
519
  }
451
520
  this.tracerProvider = null;
452
521
  }
522
+ if (this.loggerProvider) {
523
+ try {
524
+ await this.loggerProvider.forceFlush();
525
+ await this.loggerProvider.shutdown();
526
+ }
527
+ catch {
528
+ // best effort
529
+ }
530
+ this.loggerProvider = null;
531
+ }
453
532
  if (!this.sdk)
454
533
  return;
455
534
  try {
package/dist/entry.js CHANGED
File without changes
@@ -460,7 +460,7 @@
460
460
  "project.wizard.directoryPrompt": "Working directory? (optional)",
461
461
  "project.wizard.directoryHint": "Default directory for loops in this project. Leave blank to inherit",
462
462
  "project.wizard.githubSourcePrompt": "GitHub Source? (optional)",
463
- "project.wizard.githubSourceHint": "Repository in owner/repo format, e.g. CKGrafico/loop-task",
463
+ "project.wizard.githubSourceHint": "Repository in owner/repo format, e.g. PlainConceptsPlatform/loop-task",
464
464
  "project.error.updateFailed": "Failed to update project",
465
465
  "project.error.deleteFailed": "Failed to delete project",
466
466
  "project.toastCreated": "Project \"{name}\" created",
package/package.json CHANGED
@@ -1,119 +1,119 @@
1
- {
2
- "name": "@plainconceptsplatform/loop-task",
3
- "version": "2.6.0",
4
- "description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
5
- "type": "module",
6
- "bin": {
7
- "loop-task": "dist/entry.js"
8
- },
9
- "main": "dist/entry.js",
10
- "files": [
11
- "dist",
12
- "README.md",
13
- "LICENSE"
14
- ],
15
- "scripts": {
16
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node -e \"require('fs').copyFileSync('src/entry.js','dist/entry.js'); require('fs').copyFileSync('src/esm-loader.js','dist/esm-loader.js')\"",
17
- "prepublishOnly": "npm run build",
18
- "start": "node dist/entry.js",
19
- "dev": "tsx src/cli.ts",
20
- "dev:watch": "tsx --watch src/cli.ts",
21
- "board": "node dist/entry.js",
22
- "test": "vitest run --exclude '**/background-cli.test.ts'",
23
- "test:all": "vitest run",
24
- "test:watch": "vitest",
25
- "test:coverage": "vitest run --coverage --exclude '**/background-cli.test.ts'",
26
- "lint": "eslint src/ tests/",
27
- "typecheck": "tsc --noEmit",
28
- "release:dry": "npm publish --dry-run",
29
- "release": "npm publish",
30
- "visual-evidence": "tsx src/visual-evidence/cli.ts",
31
- "visual-evidence:publish": "tsx src/visual-evidence/publish.ts",
32
- "skills:mark-internal": "node scripts/mark-agent-skills-internal.mjs",
33
- "prepare": "husky"
34
- },
35
- "keywords": [
36
- "cli",
37
- "loop",
38
- "loop-engineering",
39
- "repeat",
40
- "interval",
41
- "schedule",
42
- "timer",
43
- "cron",
44
- "automation",
45
- "automations",
46
- "devops",
47
- "agent",
48
- "ai-agent",
49
- "coding-agent",
50
- "agent-automation",
51
- "claude-code",
52
- "codex",
53
- "opencode",
54
- "background-tasks",
55
- "scheduler"
56
- ],
57
- "author": "Quique Fdez Guerra",
58
- "license": "MIT",
59
- "repository": {
60
- "type": "git",
61
- "url": "https://github.com/plainconceptsplatform/loop-task.git"
62
- },
63
- "homepage": "https://github.com/plainconceptsplatform/loop-task",
64
- "bugs": {
65
- "url": "https://github.com/plainconceptsplatform/loop-task/issues"
66
- },
67
- "engines": {
68
- "node": ">=20.0.0"
69
- },
70
- "dependencies": {
71
- "@modelcontextprotocol/sdk": "^1.29.0",
72
- "@opentelemetry/api": "^1.9.1",
73
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221.0",
74
- "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0",
75
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
76
- "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
77
- "@opentelemetry/resources": "^2.10.0",
78
- "@opentelemetry/sdk-metrics": "^2.10.0",
79
- "@opentelemetry/sdk-node": "^0.221.0",
80
- "@opentelemetry/sdk-trace-base": "^2.10.0",
81
- "@opentelemetry/sdk-trace-node": "^2.10.0",
82
- "@opentelemetry/semantic-conventions": "^1.43.0",
83
- "commander": "^13.1.0",
84
- "execa": "^9.6.0",
85
- "ink": "^7.1.0",
86
- "ink-combobox": "^0.2.0",
87
- "ink-scroll-list": "^0.4.1",
88
- "ink-select-input": "^6.2.0",
89
- "ink-spinner": "^5.0.0",
90
- "ink-text-input": "^6.0.0",
91
- "inversify": "^8.1.1",
92
- "inversify-hooks": "^4.0.0",
93
- "js-yaml": "^5.2.2",
94
- "ms": "^2.1.3",
95
- "react": "^19.2.7",
96
- "yaml": "^2.9.0",
97
- "zod": "^4.4.3"
98
- },
99
- "devDependencies": {
100
- "@types/js-yaml": "^4.0.9",
101
- "@types/ms": "^2.1.0",
102
- "@types/node": "^22.15.0",
103
- "@types/react": "^19.2.17",
104
- "@vitest/coverage-v8": "^3.1.0",
105
- "eslint": "^9.25.0",
106
- "husky": "^9.1.7",
107
- "ink-testing-library": "^4.0.0",
108
- "tsx": "^4.19.0",
109
- "typescript": "^5.8.0",
110
- "typescript-eslint": "^8.30.0",
111
- "vitest": "^3.1.0"
112
- },
113
- "packageManager": "pnpm@10.30.3",
114
- "pnpm": {
115
- "overrides": {
116
- "picocolors": "1.1.0"
117
- }
118
- }
1
+ {
2
+ "name": "@plainconceptsplatform/loop-task",
3
+ "version": "2.8.0",
4
+ "description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
5
+ "type": "module",
6
+ "bin": {
7
+ "loop-task": "dist/entry.js"
8
+ },
9
+ "main": "dist/entry.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "cli",
17
+ "loop",
18
+ "loop-engineering",
19
+ "repeat",
20
+ "interval",
21
+ "schedule",
22
+ "timer",
23
+ "cron",
24
+ "automation",
25
+ "automations",
26
+ "devops",
27
+ "agent",
28
+ "ai-agent",
29
+ "coding-agent",
30
+ "agent-automation",
31
+ "claude-code",
32
+ "codex",
33
+ "opencode",
34
+ "background-tasks",
35
+ "scheduler"
36
+ ],
37
+ "author": "Plain Concepts",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/PlainConceptsPlatform/loop-task.git"
42
+ },
43
+ "homepage": "https://github.com/PlainConceptsPlatform/loop-task",
44
+ "bugs": {
45
+ "url": "https://github.com/PlainConceptsPlatform/loop-task/issues"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "registry": "https://registry.npmjs.org/"
50
+ },
51
+ "engines": {
52
+ "node": ">=20.0.0"
53
+ },
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.29.0",
56
+ "@opentelemetry/api": "^1.9.1",
57
+ "@opentelemetry/api-logs": "^0.221.0",
58
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.221.0",
59
+ "@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
60
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221.0",
61
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0",
62
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
63
+ "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
64
+ "@opentelemetry/resources": "^2.10.0",
65
+ "@opentelemetry/sdk-logs": "^0.221.0",
66
+ "@opentelemetry/sdk-metrics": "^2.10.0",
67
+ "@opentelemetry/sdk-node": "^0.221.0",
68
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
69
+ "@opentelemetry/sdk-trace-node": "^2.10.0",
70
+ "@opentelemetry/semantic-conventions": "^1.43.0",
71
+ "commander": "^13.1.0",
72
+ "execa": "^9.6.0",
73
+ "ink": "^7.1.0",
74
+ "ink-combobox": "^0.2.0",
75
+ "ink-scroll-list": "^0.4.1",
76
+ "ink-select-input": "^6.2.0",
77
+ "ink-spinner": "^5.0.0",
78
+ "ink-text-input": "^6.0.0",
79
+ "inversify": "^8.1.1",
80
+ "inversify-hooks": "^4.0.0",
81
+ "js-yaml": "^5.2.2",
82
+ "ms": "^2.1.3",
83
+ "react": "^19.2.7",
84
+ "yaml": "^2.9.0",
85
+ "zod": "^4.4.3"
86
+ },
87
+ "devDependencies": {
88
+ "@types/js-yaml": "^4.0.9",
89
+ "@types/ms": "^2.1.0",
90
+ "@types/node": "^22.15.0",
91
+ "@types/react": "^19.2.17",
92
+ "@vitest/coverage-v8": "^3.1.0",
93
+ "eslint": "^9.25.0",
94
+ "husky": "^9.1.7",
95
+ "ink-testing-library": "^4.0.0",
96
+ "tsx": "^4.19.0",
97
+ "typescript": "^5.8.0",
98
+ "typescript-eslint": "^8.30.0",
99
+ "vitest": "^3.1.0"
100
+ },
101
+ "scripts": {
102
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node -e \"require('fs').copyFileSync('src/entry.js','dist/entry.js'); require('fs').copyFileSync('src/esm-loader.js','dist/esm-loader.js')\"",
103
+ "start": "node dist/entry.js",
104
+ "dev": "tsx src/cli.ts",
105
+ "dev:watch": "tsx --watch src/cli.ts",
106
+ "board": "node dist/entry.js",
107
+ "test": "vitest run --exclude '**/background-cli.test.ts'",
108
+ "test:all": "vitest run",
109
+ "test:watch": "vitest",
110
+ "test:coverage": "vitest run --coverage --exclude '**/background-cli.test.ts'",
111
+ "lint": "eslint src/ tests/",
112
+ "typecheck": "tsc --noEmit",
113
+ "release:dry": "npm publish --dry-run --access public",
114
+ "release": "npm publish --access public",
115
+ "visual-evidence": "tsx src/visual-evidence/cli.ts",
116
+ "visual-evidence:publish": "tsx src/visual-evidence/publish.ts",
117
+ "skills:mark-internal": "node scripts/mark-agent-skills-internal.mjs"
118
+ }
119
119
  }