@plainconceptsplatform/loop-task 2.6.0 → 2.6.1

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.
@@ -1,11 +1,24 @@
1
+ import { execa } from "execa";
2
+ import { daemonLog } from "../../daemon-log.js";
1
3
  const OPENCODE_BINARIES = ["opencode"];
4
+ const SERVE_PORT = 4096;
5
+ const SERVE_URL = `http://localhost:${SERVE_PORT}`;
6
+ const HEALTH_CHECK_TIMEOUT_MS = 120_000;
7
+ const HEALTH_CHECK_INTERVAL_MS = 1_000;
8
+ const SHUTDOWN_TIMEOUT_MS = 5_000;
2
9
  /**
3
10
  * Detects `opencode run ...` invocations and enables OpenCode's
4
11
  * native OpenTelemetry support, routing to loop-task's endpoint.
12
+ *
13
+ * Also manages a persistent `opencode serve` sidecar process so that
14
+ * `opencode run` tasks delegate to a warm serve instance via `--attach`,
15
+ * eliminating per-task cold-start time.
5
16
  */
6
17
  export class OpenCodeTelemetryIntegration {
7
18
  constructor() {
8
19
  this.id = "opencode";
20
+ this.serveProcess = null;
21
+ this.serveInfo = null;
9
22
  }
10
23
  matches(command, args) {
11
24
  const basename = command.split("/").pop()?.replace(/\.exe$/, "").toLowerCase() ?? "";
@@ -15,19 +28,53 @@ export class OpenCodeTelemetryIntegration {
15
28
  const env = { ...invocation.env };
16
29
  // Enable OpenCode's experimental OpenTelemetry
17
30
  env.OPENCODE_EXPERIMENTAL_OPEN_TELEMETRY = "true";
18
- // Adapt the OTLP endpoint/proprotocol from the parent context
19
- // OpenCode currently reads the standard OTEL_* environment variables
20
- // which are already set by the adapter's prepareChildProcess
21
- // We just need to make sure the integration is activated.
22
31
  return { command: invocation.command, args: [...invocation.args], env };
23
32
  }
24
33
  parseUsage(result) {
25
34
  if (result.exitCode !== 0 || !result.stdout)
26
35
  return undefined;
27
36
  try {
28
- // OpenCode may output JSON with --format json
29
- // Try to parse structured output for usage data
37
+ // With --format json, opencode emits JSONL. The last step_finish event
38
+ // has token/cost info. Try parsing as JSONL first.
30
39
  const lines = result.stdout.trim().split("\n");
40
+ const jsonlLines = [];
41
+ let allJson = true;
42
+ for (const line of lines) {
43
+ try {
44
+ jsonlLines.push(JSON.parse(line));
45
+ }
46
+ catch {
47
+ allJson = false;
48
+ break;
49
+ }
50
+ }
51
+ if (allJson && jsonlLines.length > 0) {
52
+ // Find the last step_finish event
53
+ let accumulated = {};
54
+ for (const evt of jsonlLines) {
55
+ if (typeof evt === "object" && evt !== null && evt.type === "step_finish") {
56
+ const part = evt.part;
57
+ const tokens = part?.tokens;
58
+ if (tokens) {
59
+ accumulated.inputTokens = (accumulated.inputTokens ?? 0) + (typeof tokens.input === "number" ? tokens.input : 0);
60
+ accumulated.outputTokens = (accumulated.outputTokens ?? 0) + (typeof tokens.output === "number" ? tokens.output : 0);
61
+ const cache = tokens.cache;
62
+ if (cache) {
63
+ accumulated.cacheReadTokens = (accumulated.cacheReadTokens ?? 0) + (typeof cache.read === "number" ? cache.read : 0);
64
+ accumulated.cacheWriteTokens = (accumulated.cacheWriteTokens ?? 0) + (typeof cache.write === "number" ? cache.write : 0);
65
+ }
66
+ }
67
+ if (typeof part?.cost === "number") {
68
+ accumulated.costUsd = (accumulated.costUsd ?? 0) + part.cost;
69
+ }
70
+ }
71
+ }
72
+ if (Object.keys(accumulated).length > 0) {
73
+ accumulated.integration = "opencode";
74
+ return accumulated;
75
+ }
76
+ }
77
+ // Fallback: try parsing the last line as JSON (legacy --format json without JSONL)
31
78
  const lastLine = lines[lines.length - 1];
32
79
  if (!lastLine)
33
80
  return undefined;
@@ -63,10 +110,134 @@ export class OpenCodeTelemetryIntegration {
63
110
  usage.sessionId = parsed.sessionId;
64
111
  if (Object.keys(usage).length === 0)
65
112
  return undefined;
113
+ usage.integration = "opencode";
66
114
  return usage;
67
115
  }
68
116
  catch {
69
117
  return undefined;
70
118
  }
71
119
  }
120
+ async ensureServe(cwd, env) {
121
+ // Already running and healthy
122
+ if (this.serveInfo && this.isServeAlive()) {
123
+ return this.serveInfo;
124
+ }
125
+ try {
126
+ daemonLog("opencode-serve: starting opencode serve sidecar");
127
+ this.serveProcess = execa("opencode", [
128
+ "serve",
129
+ "--port", String(SERVE_PORT),
130
+ "--hostname", "127.0.0.1",
131
+ ], {
132
+ cwd,
133
+ env: { ...process.env, ...env },
134
+ stdout: "pipe",
135
+ stderr: "pipe",
136
+ buffer: false,
137
+ detached: false,
138
+ });
139
+ this.serveProcess.catch((err) => {
140
+ daemonLog(`opencode-serve: process error: ${String(err)}`);
141
+ this.serveProcess = null;
142
+ this.serveInfo = null;
143
+ });
144
+ this.serveProcess.on("exit", (code) => {
145
+ daemonLog(`opencode-serve: process exited with code ${code}`);
146
+ this.serveProcess = null;
147
+ this.serveInfo = null;
148
+ });
149
+ // Wait for health check
150
+ const healthy = await this.waitForHealth();
151
+ if (healthy) {
152
+ const pid = this.serveProcess?.pid;
153
+ if (pid) {
154
+ this.serveInfo = { port: SERVE_PORT, pid, url: SERVE_URL };
155
+ daemonLog(`opencode-serve: healthy, pid=${pid}, port=${SERVE_PORT}`);
156
+ return this.serveInfo;
157
+ }
158
+ }
159
+ daemonLog("opencode-serve: health check timed out");
160
+ return;
161
+ }
162
+ catch (err) {
163
+ daemonLog(`opencode-serve: failed to start: ${String(err)}`);
164
+ this.serveProcess = null;
165
+ this.serveInfo = null;
166
+ return;
167
+ }
168
+ }
169
+ isServeAlive() {
170
+ // Fast check: if we don't have a process reference, serve is not alive
171
+ if (!this.serveProcess || !this.serveInfo)
172
+ return false;
173
+ return true;
174
+ }
175
+ prepareRunArgs(args, cwd) {
176
+ const modified = [...args];
177
+ // Inject --attach after "run" (args[0] === "run")
178
+ const runIndex = modified.indexOf("run");
179
+ if (runIndex === -1)
180
+ return modified;
181
+ // Check if --attach is already present
182
+ const hasAttach = modified.includes("--attach");
183
+ if (!hasAttach) {
184
+ modified.splice(runIndex + 1, 0, "--attach", SERVE_URL);
185
+ }
186
+ // Inject --dir so serve knows which directory to work in
187
+ const hasDir = modified.includes("--dir");
188
+ if (!hasDir && cwd) {
189
+ // Find the position after --attach (or after "run" if --attach wasn't added by us)
190
+ const attachIdx = modified.indexOf("--attach");
191
+ const insertPos = attachIdx >= 0 ? attachIdx + 2 : runIndex + 1;
192
+ modified.splice(insertPos, 0, "--dir", cwd);
193
+ }
194
+ return modified;
195
+ }
196
+ async shutdownServe() {
197
+ if (!this.serveProcess)
198
+ return;
199
+ daemonLog("opencode-serve: shutting down serve process");
200
+ const proc = this.serveProcess;
201
+ this.serveProcess = null;
202
+ this.serveInfo = null;
203
+ try {
204
+ proc.kill("SIGTERM");
205
+ await Promise.race([
206
+ proc,
207
+ new Promise((_, reject) => setTimeout(() => reject(new Error("shutdown timeout")), SHUTDOWN_TIMEOUT_MS)),
208
+ ]);
209
+ }
210
+ catch {
211
+ // Force kill if SIGTERM didn't work
212
+ try {
213
+ proc.kill("SIGKILL");
214
+ }
215
+ catch {
216
+ // best effort
217
+ }
218
+ }
219
+ }
220
+ async waitForHealth() {
221
+ const start = Date.now();
222
+ while (Date.now() - start < HEALTH_CHECK_TIMEOUT_MS) {
223
+ if (!this.serveProcess)
224
+ return false; // process exited
225
+ try {
226
+ const controller = new AbortController();
227
+ const timeout = setTimeout(() => controller.abort(), 3_000);
228
+ const res = await fetch(`${SERVE_URL}/global/health`, { signal: controller.signal });
229
+ clearTimeout(timeout);
230
+ if (res.ok) {
231
+ const data = await res.json();
232
+ if (data.healthy === true)
233
+ return true;
234
+ }
235
+ }
236
+ catch {
237
+ // server not ready yet
238
+ }
239
+ await new Promise((r) => setTimeout(r, HEALTH_CHECK_INTERVAL_MS));
240
+ }
241
+ return false;
242
+ }
72
243
  }
@@ -0,0 +1,78 @@
1
+ import { daemonLog } from "../daemon-log.js";
2
+ import { getAgentIntegrations } from "./agent-integrations/index.js";
3
+ /**
4
+ * Manages the lifecycle of persistent agent serve sidecars (e.g., opencode serve).
5
+ *
6
+ * On daemon boot, starts serve processes for integrations that support them.
7
+ * On daemon shutdown, gracefully stops all serve processes.
8
+ *
9
+ * The serve manager is the single owner of serve process lifecycle.
10
+ * The TelemetryManager and command-runner reference the integration's
11
+ * isServeAlive() to check state, but never start/stop serve processes directly.
12
+ */
13
+ export class AgentServeManager {
14
+ constructor(settings, telemetryManager) {
15
+ this.settings = settings;
16
+ this.telemetryManager = telemetryManager;
17
+ this.integrations = [];
18
+ this.integrations = [...getAgentIntegrations()];
19
+ }
20
+ /**
21
+ * Start serve sidecars for all integrations that support them.
22
+ * Called on daemon boot.
23
+ */
24
+ async ensureAllServe(cwd) {
25
+ const endpoint = this.settings.telemetryEndpoint;
26
+ if (!endpoint) {
27
+ daemonLog("agent-serve-manager: no telemetry endpoint, skipping serve startup");
28
+ return;
29
+ }
30
+ // Build static OTEL env for serve processes
31
+ const serveEnv = {
32
+ OTEL_EXPORTER_OTLP_ENDPOINT: endpoint,
33
+ OTEL_EXPORTER_OTLP_PROTOCOL: this.settings.telemetryProtocol,
34
+ OTEL_TRACES_EXPORTER: "otlp",
35
+ OTEL_METRICS_EXPORTER: "otlp",
36
+ OTEL_LOGS_EXPORTER: "otlp",
37
+ OTEL_RESOURCE_ATTRIBUTES: `service.name=${this.settings.telemetryServiceName}`,
38
+ };
39
+ if (this.settings.telemetryAutoInstrumentAgents) {
40
+ serveEnv.OPENCODE_EXPERIMENTAL_OPEN_TELEMETRY = "true";
41
+ serveEnv.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
42
+ }
43
+ for (const integration of this.integrations) {
44
+ if (!integration.ensureServe)
45
+ continue;
46
+ try {
47
+ daemonLog(`agent-serve-manager: ensuring serve for integration "${integration.id}"`);
48
+ const info = await integration.ensureServe(cwd, serveEnv);
49
+ if (info) {
50
+ daemonLog(`agent-serve-manager: serve started for "${integration.id}", pid=${info.pid}, port=${info.port}`);
51
+ }
52
+ else {
53
+ daemonLog(`agent-serve-manager: serve not started for "${integration.id}" (not supported or failed)`);
54
+ }
55
+ }
56
+ catch (err) {
57
+ daemonLog(`agent-serve-manager: failed to start serve for "${integration.id}": ${String(err)}`);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Gracefully shut down all managed serve processes.
63
+ * Called on daemon shutdown.
64
+ */
65
+ async shutdownAll() {
66
+ for (const integration of this.integrations) {
67
+ if (!integration.shutdownServe)
68
+ continue;
69
+ try {
70
+ daemonLog(`agent-serve-manager: shutting down serve for "${integration.id}"`);
71
+ await integration.shutdownServe();
72
+ }
73
+ catch (err) {
74
+ daemonLog(`agent-serve-manager: error shutting down "${integration.id}": ${String(err)}`);
75
+ }
76
+ }
77
+ }
78
+ }
@@ -39,6 +39,7 @@ export class NoopTelemetryAdapter {
39
39
  recordRetry() { }
40
40
  recordFailure() { }
41
41
  recordAgentUsage() { }
42
+ logEvent() { }
42
43
  prepareChildProcess(_invocation, _context, _integrationOverride) {
43
44
  return { env: {} };
44
45
  }
@@ -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",