@p4code/cli 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -236,7 +236,7 @@ const make$76 = () => {
236
236
  const layer$72 = Layer.sync(NetService, make$76);
237
237
  //#endregion
238
238
  //#region package.json
239
- var version = "0.1.38";
239
+ var version = "0.1.40";
240
240
  //#endregion
241
241
  //#region src/config.ts
242
242
  /**
@@ -19251,6 +19251,8 @@ No self-reference. Never name or announce the style. No compressed answer plus n
19251
19251
 
19252
19252
  ## Auto-Clarity
19253
19253
 
19254
+ Warnings are substance, never fluff. A destructive or irreversible request (deleting data, prod mutations, force-push, secret exposure) always gets the uncompressed warning block, even when the user tells you to skip it. The warning is complete sentences from its heading to its last word; compression never decides whether it appears.
19255
+
19254
19256
  Drop compression and write normally when:
19255
19257
  - Security warnings
19256
19258
  - Irreversible action confirmations
@@ -24232,7 +24234,16 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
24232
24234
  return;
24233
24235
  }
24234
24236
  case "thread.turn-interrupt-requested": {
24235
- if (event.payload.turnId === void 0) return;
24237
+ if (event.payload.turnId === void 0) {
24238
+ const threadTurns = yield* projectionTurnRepository.listByThreadId({ threadId: event.payload.threadId });
24239
+ yield* Effect.forEach(threadTurns.filter((turn) => turn.turnId !== null && turn.state === "running"), (turn) => turn.turnId === null ? Effect.void : projectionTurnRepository.upsertByTurnId({
24240
+ ...turn,
24241
+ turnId: turn.turnId,
24242
+ state: "interrupted",
24243
+ completedAt: turn.completedAt ?? event.payload.createdAt
24244
+ }), { concurrency: 1 });
24245
+ return;
24246
+ }
24236
24247
  const existingTurn = yield* projectionTurnRepository.getByTurnId({
24237
24248
  threadId: event.payload.threadId,
24238
24249
  turnId: event.payload.turnId
@@ -24282,7 +24293,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
24282
24293
  yield* projectionTurnRepository.upsertByTurnId({
24283
24294
  ...existingTurn.value,
24284
24295
  assistantMessageId: event.payload.assistantMessageId,
24285
- state: turnStillRunning ? existingTurn.value.state : nextState,
24296
+ state: turnStillRunning ? existingTurn.value.state : existingTurn.value.state === "interrupted" ? "interrupted" : nextState,
24286
24297
  checkpointTurnCount: event.payload.checkpointTurnCount,
24287
24298
  checkpointRef: event.payload.checkpointRef,
24288
24299
  checkpointStatus: event.payload.status,
@@ -32264,6 +32275,12 @@ const staticAndDevRouteLayer = HttpRouter.add("GET", "*", Effect.gen(function* (
32264
32275
  contentType
32265
32276
  });
32266
32277
  }));
32278
+ //#endregion
32279
+ //#region src/provider/Services/ProviderService.ts
32280
+ /**
32281
+ * ProviderService - Service tag for provider orchestration.
32282
+ */
32283
+ var ProviderService = class extends Context.Service()("@p4code/cli/provider/Services/ProviderService") {};
32267
32284
  (() => {
32268
32285
  try {
32269
32286
  return process.env.NODE_ENV === "development";
@@ -32798,25 +32815,6 @@ function createContentGroup(type, deletionLineIndex, additionLineIndex) {
32798
32815
  }
32799
32816
  //#endregion
32800
32817
  //#region src/checkpointing/ThreadChangedFiles.ts
32801
- /**
32802
- * Per-thread changed-file attribution for checkpoint diffs.
32803
- *
32804
- * A checkpoint snapshots the whole working tree (`git add -A`), so on a shared
32805
- * worktree the diff between two of a thread's checkpoints also contains every
32806
- * edit other threads made in the window. The thread's own tool activities are
32807
- * per-thread by construction, and the inline changed-files summary the clients
32808
- * derive from them is already the surface users trust. These helpers extract
32809
- * that same path list on the server and cut a checkpoint diff down to it.
32810
- *
32811
- * `collectActivityChangedFilePaths` must stay in lockstep with
32812
- * `collectChangedFiles` in `apps/web/src/session-logic.ts` (and the mobile
32813
- * copy in `threadActivity.ts`): same field names, same nested keys, same depth
32814
- * bound. The one deliberate difference is dropping the cap of 12 paths - that
32815
- * cap exists for display, and a filter that stopped collecting would silently
32816
- * drop real work from the diff.
32817
- *
32818
- * @module ThreadChangedFiles
32819
- */
32820
32818
  const CHANGED_FILE_FIELDS = [
32821
32819
  "path",
32822
32820
  "filePath",
@@ -32885,6 +32883,16 @@ function normalizeChangedFilePaths(changedPaths, workspaceCwd) {
32885
32883
  }
32886
32884
  return normalized;
32887
32885
  }
32886
+ /**
32887
+ * Whether another thread's live session is bound to the same workspace
32888
+ * directory. This is the shared-worktree signal: a thread whose activities
32889
+ * name no paths keeps its unfiltered diff only while it is alone in the
32890
+ * directory, because with a concurrent neighbour the unfiltered tree diff
32891
+ * would present the neighbour's edits as this thread's own.
32892
+ */
32893
+ function otherSessionSharesWorkspace(sessions, threadId, workspaceCwd) {
32894
+ return sessions.some((session) => session.threadId !== threadId && session.cwd === workspaceCwd);
32895
+ }
32888
32896
  const DIFF_FILE_HEADER = /^diff --git /gm;
32889
32897
  function splitUnifiedDiffBlocks(diff) {
32890
32898
  const starts = [];
@@ -35517,6 +35525,7 @@ const make$40 = Effect.gen(function* () {
35517
35525
  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
35518
35526
  const checkpointStore = yield* CheckpointStore;
35519
35527
  const threadActivities = yield* ProjectionThreadActivityRepository;
35528
+ const providerService = yield* ProviderService;
35520
35529
  /**
35521
35530
  * The file paths this thread's own tool activities name, optionally limited
35522
35531
  * to a set of turns. This is the attribution a checkpoint cannot provide: a
@@ -35535,13 +35544,17 @@ const make$40 = Effect.gen(function* () {
35535
35544
  return changedPaths;
35536
35545
  });
35537
35546
  /**
35538
- * A thread whose activities name no paths at all gets the unfiltered diff.
35539
- * That is the deliberate escape valve: a thread working purely through shell
35540
- * commands never names the files it touched, and erasing its whole diff
35541
- * would be worse than the shared-worktree contamination this filter exists
35542
- * to remove.
35547
+ * A thread whose activities name no paths at all (shell-only work) keeps
35548
+ * the unfiltered diff only while no other live session shares the
35549
+ * directory. Alone, the whole tree diff is the thread's own work and
35550
+ * erasing it would lose real changes; with a concurrent neighbour the same
35551
+ * diff presents the neighbour's edits as this thread's, so it is
35552
+ * suppressed instead.
35543
35553
  */
35544
- const attributeDiffToThread = (diff, changedPaths, workspaceCwd) => changedPaths.size === 0 ? diff : filterUnifiedDiffToChangedFiles(diff, changedPaths, workspaceCwd);
35554
+ const attributeDiffToThread = Effect.fn("CheckpointDiffQuery.attributeDiffToThread")(function* (threadId, diff, changedPaths, workspaceCwd) {
35555
+ if (changedPaths.size > 0) return filterUnifiedDiffToChangedFiles(diff, changedPaths, workspaceCwd);
35556
+ return otherSessionSharesWorkspace(yield* providerService.listSessions(), threadId, workspaceCwd) ? "" : diff;
35557
+ });
35545
35558
  const getTurnDiff = Effect.fn("getTurnDiff")(function* (input) {
35546
35559
  const operation = "CheckpointDiffQuery.getTurnDiff";
35547
35560
  const ignoreWhitespace = input.ignoreWhitespace ?? true;
@@ -35604,7 +35617,7 @@ const make$40 = Effect.gen(function* () {
35604
35617
  }).pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints"));
35605
35618
  const turnIdsInRange = new Set(threadContext.value.checkpoints.filter((checkpoint) => checkpoint.checkpointTurnCount > input.fromTurnCount && checkpoint.checkpointTurnCount <= input.toTurnCount).map((checkpoint) => checkpoint.turnId));
35606
35619
  const changedPaths = yield* collectThreadChangedFilePaths(input.threadId, turnIdsInRange);
35607
- const turnDiff = buildTurnDiffResult(input, attributeDiffToThread(diff, changedPaths, workspaceCwd));
35620
+ const turnDiff = buildTurnDiffResult(input, yield* attributeDiffToThread(input.threadId, diff, changedPaths, workspaceCwd));
35608
35621
  if (!isTurnDiffResult(turnDiff)) return yield* new CheckpointDiffResultInvalidError({
35609
35622
  operation,
35610
35623
  threadId: input.threadId
@@ -35667,7 +35680,7 @@ const make$40 = Effect.gen(function* () {
35667
35680
  threadId: input.threadId,
35668
35681
  fromTurnCount: 0,
35669
35682
  toTurnCount: input.toTurnCount
35670
- }, attributeDiffToThread(diff, changedPaths, workspaceCwd));
35683
+ }, yield* attributeDiffToThread(input.threadId, diff, changedPaths, workspaceCwd));
35671
35684
  if (!isTurnDiffResult(turnDiff)) return yield* new CheckpointDiffResultInvalidError({
35672
35685
  operation,
35673
35686
  threadId: input.threadId
@@ -49392,12 +49405,6 @@ const ProviderEventLoggersLive = Layer.effect(ProviderEventLoggers, Effect.gen(f
49392
49405
  };
49393
49406
  }));
49394
49407
  //#endregion
49395
- //#region src/provider/Services/ProviderService.ts
49396
- /**
49397
- * ProviderService - Service tag for provider orchestration.
49398
- */
49399
- var ProviderService = class extends Context.Service()("@p4code/cli/provider/Services/ProviderService") {};
49400
- //#endregion
49401
49408
  //#region src/mcp/McpProviderSession.ts
49402
49409
  /**
49403
49410
  * The name p4code declares its own toolkit under, in every provider.
@@ -52299,6 +52306,37 @@ function formatAskUserQuestionAnswers(answers) {
52299
52306
  return formatted;
52300
52307
  }
52301
52308
  //#endregion
52309
+ //#region src/provider/Layers/ClaudePromptAppends.ts
52310
+ /**
52311
+ * Setting-driven system prompt appends for the Claude adapter.
52312
+ *
52313
+ * Kept out of `ClaudeAdapter.ts` so their regression evals
52314
+ * (`ClaudePromptAppends.evals.ts`) and any future prompt constant of the same
52315
+ * shape can import the text without dragging in the adapter's whole module
52316
+ * graph. The join point is `ClaudeAdapter.ts`, which appends these to the
52317
+ * claude_code preset system prompt per setting.
52318
+ */
52319
+ /**
52320
+ * Appended to the preset system prompt when `narrateBeforeTools` is on. The SDK
52321
+ * preset drops the interactive CLI's terminal-tone sections, so without this the
52322
+ * model opens a turn with a tool call rather than a line saying what it is doing.
52323
+ */
52324
+ const NARRATE_BEFORE_TOOLS_PROMPT = "Before your first tool call in a turn, write one short sentence saying what you are about to do. Keep it to a single line and skip it when you are answering without tools.";
52325
+ /**
52326
+ * Said whichever way the setting is set, and said as an override.
52327
+ *
52328
+ * The preset system prompt this adapter requests already carries an
52329
+ * instruction not to spawn subagents unless the user asked — and a preset is
52330
+ * taken wholesale, so there is no version of it with that line removed. An
52331
+ * append that merely stayed silent when subagents are allowed would therefore
52332
+ * leave the restriction standing, which is the one outcome the setting exists
52333
+ * to change. Both texts name the earlier instruction they replace, because a
52334
+ * later instruction that does not acknowledge the conflict reads as an
52335
+ * accident rather than a decision.
52336
+ */
52337
+ const SUBAGENTS_ALLOWED_PROMPT = "You may spawn subagents with the Task tool whenever it helps, without asking the user first. This overrides any earlier instruction to use subagents only when they are requested. Prefer one for bounded search or review work whose result is much smaller than the reading it takes to produce; do the work inline when the answer is a single file or a single edit.";
52338
+ const SUBAGENTS_ON_REQUEST_PROMPT = "Do not spawn subagents with the Task tool unless the user asks for one. This overrides any earlier instruction permitting them on your own initiative. Do the work inline instead, and say so if it would have been better delegated.";
52339
+ //#endregion
52302
52340
  //#region src/provider/Layers/ClaudeAdapter.ts
52303
52341
  /**
52304
52342
  * ClaudeAdapterLive - Scoped live implementation for the Claude Agent provider adapter.
@@ -52852,28 +52890,8 @@ const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
52852
52890
  "image/png",
52853
52891
  "image/webp"
52854
52892
  ]);
52855
- /**
52856
- * Appended to the preset system prompt when `narrateBeforeTools` is on. The SDK
52857
- * preset drops the interactive CLI's terminal-tone sections, so without this the
52858
- * model opens a turn with a tool call rather than a line saying what it is doing.
52859
- */
52860
52893
  /** Emitted by the CLI but absent from the SDK's `SDKMessage` union. */
52861
52894
  const UNTYPED_COMMAND_LIFECYCLE_MESSAGE = "command_lifecycle";
52862
- const NARRATE_BEFORE_TOOLS_PROMPT = "Before your first tool call in a turn, write one short sentence saying what you are about to do. Keep it to a single line and skip it when you are answering without tools.";
52863
- /**
52864
- * Said whichever way the setting is set, and said as an override.
52865
- *
52866
- * The preset system prompt this adapter requests already carries an
52867
- * instruction not to spawn subagents unless the user asked — and a preset is
52868
- * taken wholesale, so there is no version of it with that line removed. An
52869
- * append that merely stayed silent when subagents are allowed would therefore
52870
- * leave the restriction standing, which is the one outcome the setting exists
52871
- * to change. Both texts name the earlier instruction they replace, because a
52872
- * later instruction that does not acknowledge the conflict reads as an
52873
- * accident rather than a decision.
52874
- */
52875
- const SUBAGENTS_ALLOWED_PROMPT = "You may spawn subagents with the Task tool whenever it helps, without asking the user first. This overrides any earlier instruction to use subagents only when they are requested. Prefer one for bounded search or review work whose result is much smaller than the reading it takes to produce; do the work inline when the answer is a single file or a single edit.";
52876
- const SUBAGENTS_ON_REQUEST_PROMPT = "Do not spawn subagents with the Task tool unless the user asks for one. This overrides any earlier instruction permitting them on your own initiative. Do the work inline instead, and say so if it would have been better delegated.";
52877
52895
  const CLAUDE_SETTING_SOURCES = [
52878
52896
  "user",
52879
52897
  "project",
@@ -53730,6 +53748,27 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
53730
53748
  message: pending ? mergeUserMessages(pending.message, merged) : merged
53731
53749
  };
53732
53750
  });
53751
+ /**
53752
+ * Release the turn from a question it is blocked on, so Stop can end it.
53753
+ *
53754
+ * A pending AskUserQuestion holds the SDK inside `canUseTool`, and that
53755
+ * callback only aborts when the CLI cancels its own permission request -
53756
+ * which a stop cannot be counted on to produce, since the CLI is already
53757
+ * waiting on our answer when the interrupt reaches it. Denying the call here
53758
+ * ends the tool use locally, so the turn reaches its result and settles as
53759
+ * interrupted instead of sitting on an open question.
53760
+ */
53761
+ const cancelPendingUserInputs = Effect.fnUntraced(function* (context) {
53762
+ const pending = Array.from(context.pendingUserInputs.values());
53763
+ context.pendingUserInputs.clear();
53764
+ yield* Effect.forEach(pending, (entry) => {
53765
+ entry.cancelled = true;
53766
+ return Deferred.succeed(entry.answers, {});
53767
+ }, {
53768
+ concurrency: 1,
53769
+ discard: true
53770
+ });
53771
+ });
53733
53772
  const handleStreamEvent = Effect.fn("handleStreamEvent")(function* (context, message) {
53734
53773
  if (message.type !== "stream_event") return;
53735
53774
  const { event } = message;
@@ -54537,10 +54576,10 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
54537
54576
  multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : false
54538
54577
  }));
54539
54578
  const answersDeferred = yield* Deferred.make();
54540
- let aborted = false;
54541
54579
  const pendingInput = {
54542
54580
  questions,
54543
- answers: answersDeferred
54581
+ answers: answersDeferred,
54582
+ cancelled: false
54544
54583
  };
54545
54584
  const requestedStamp = yield* makeEventStamp();
54546
54585
  yield* offerRuntimeEvent({
@@ -54565,7 +54604,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
54565
54604
  pendingUserInputs.set(requestId, pendingInput);
54566
54605
  const onAbort = () => {
54567
54606
  if (!pendingUserInputs.has(requestId)) return;
54568
- aborted = true;
54607
+ pendingInput.cancelled = true;
54569
54608
  pendingUserInputs.delete(requestId);
54570
54609
  runFork(Deferred.succeed(answersDeferred, {}));
54571
54610
  };
@@ -54589,7 +54628,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
54589
54628
  payload: { answers }
54590
54629
  }
54591
54630
  });
54592
- if (aborted) return {
54631
+ if (pendingInput.cancelled) return {
54593
54632
  behavior: "deny",
54594
54633
  message: "User cancelled tool execution."
54595
54634
  };
@@ -54953,6 +54992,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
54953
54992
  const context = yield* requireSession(threadId);
54954
54993
  context.interruptRequested = true;
54955
54994
  yield* reclaimSteeredMessages(context);
54995
+ yield* cancelPendingUserInputs(context);
54956
54996
  yield* Effect.tryPromise({
54957
54997
  try: () => context.query.interrupt(),
54958
54998
  catch: (cause) => toRequestError$1(threadId, "turn/interrupt", cause)
@@ -90865,7 +90905,7 @@ const make$2 = Effect.gen(function* () {
90865
90905
  createdAt: input.createdAt
90866
90906
  })));
90867
90907
  /**
90868
- * A successful interrupt otherwise leaves nothing behind: the only row the
90908
+ * A stop otherwise leaves nothing behind: the only row the
90869
90909
  * timeline ever got was the failure case above, and the turn fold's "You
90870
90910
  * stopped after Xs" is invisible while the fold is open and says nothing in
90871
90911
  * the work log. The turn id comes from the request rather than the provider
@@ -91352,12 +91392,19 @@ const make$2 = Effect.gen(function* () {
91352
91392
  turnId: event.payload.turnId ?? null,
91353
91393
  createdAt: event.payload.createdAt
91354
91394
  });
91355
- yield* providerService.interruptTurn({ threadId: event.payload.threadId });
91356
91395
  yield* appendTurnInterruptedActivity({
91357
91396
  threadId: event.payload.threadId,
91358
91397
  turnId: event.payload.turnId ?? null,
91359
91398
  createdAt: event.payload.createdAt
91360
91399
  });
91400
+ yield* providerService.interruptTurn({ threadId: event.payload.threadId }).pipe(Effect.catchCause((cause) => appendProviderFailureActivity({
91401
+ threadId: event.payload.threadId,
91402
+ kind: "provider.turn.interrupt.failed",
91403
+ summary: "Provider turn interrupt failed",
91404
+ detail: formatFailureDetail(cause),
91405
+ turnId: event.payload.turnId ?? null,
91406
+ createdAt: event.payload.createdAt
91407
+ })));
91361
91408
  });
91362
91409
  const processApprovalResponseRequested = Effect.fn("processApprovalResponseRequested")(function* (event) {
91363
91410
  const thread = yield* resolveThread(event.payload.threadId);
@@ -91620,13 +91667,14 @@ const make$1 = Effect.gen(function* () {
91620
91667
  }
91621
91668
  return normalizeChangedFilePaths(changedPaths, input.cwd);
91622
91669
  }), Effect.orElseSucceed(() => /* @__PURE__ */ new Set()));
91670
+ const suppressUnattributedFiles = turnChangedPaths.size === 0 && otherSessionSharesWorkspace(yield* providerService.listSessions(), input.threadId, input.cwd);
91623
91671
  const files = yield* checkpointStore.diffCheckpoints({
91624
91672
  cwd: input.cwd,
91625
91673
  fromCheckpointRef,
91626
91674
  toCheckpointRef: targetCheckpointRef,
91627
91675
  fallbackFromToHead: false,
91628
91676
  ignoreWhitespace: false
91629
- }).pipe(Effect.map((diff) => parseTurnDiffFilesFromUnifiedDiff(diff).filter((file) => turnChangedPaths.size === 0 || turnChangedPaths.has(file.path)).map((file) => ({
91677
+ }).pipe(Effect.map((diff) => parseTurnDiffFilesFromUnifiedDiff(diff).filter((file) => turnChangedPaths.size === 0 ? !suppressUnattributedFiles : turnChangedPaths.has(file.path)).map((file) => ({
91630
91678
  path: file.path,
91631
91679
  kind: "modified",
91632
91680
  additions: file.additions,