@gajae-code/coding-agent 0.7.2 → 0.7.3

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 (52) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli.d.ts +6 -0
  4. package/dist/types/commands/mcp.d.ts +70 -0
  5. package/dist/types/config/keybindings.d.ts +2 -2
  6. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  7. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  8. package/dist/types/modes/components/model-selector.d.ts +2 -0
  9. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  10. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  11. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  12. package/dist/types/notifications/telegram-daemon.d.ts +54 -16
  13. package/dist/types/notifications/topic-registry.d.ts +2 -0
  14. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  15. package/dist/types/web/insane/url-guard.d.ts +6 -3
  16. package/dist/types/web/scrapers/types.d.ts +5 -0
  17. package/dist/types/web/scrapers/utils.d.ts +7 -1
  18. package/package.json +7 -7
  19. package/src/cli/mcp-cli.ts +272 -0
  20. package/src/cli.ts +6 -2
  21. package/src/commands/mcp.ts +117 -0
  22. package/src/config/keybindings.ts +2 -2
  23. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  24. package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
  25. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  26. package/src/extensibility/extensions/runner.ts +1 -0
  27. package/src/gjc-runtime/tmux-common.ts +3 -1
  28. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  29. package/src/hooks/skill-state.ts +57 -0
  30. package/src/internal-urls/docs-index.generated.ts +10 -7
  31. package/src/modes/bridge/bridge-mode.ts +11 -0
  32. package/src/modes/components/custom-editor.ts +2 -0
  33. package/src/modes/components/footer.ts +2 -3
  34. package/src/modes/components/model-selector.ts +12 -0
  35. package/src/modes/components/status-line/git-utils.ts +25 -0
  36. package/src/modes/components/status-line.ts +10 -11
  37. package/src/modes/components/welcome.ts +2 -3
  38. package/src/modes/controllers/selector-controller.ts +3 -0
  39. package/src/modes/interactive-mode.ts +2 -1
  40. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  41. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  42. package/src/modes/theme/defaults/index.ts +2 -0
  43. package/src/notifications/operator-runtime.ts +171 -0
  44. package/src/notifications/telegram-daemon.ts +347 -251
  45. package/src/notifications/topic-registry.ts +5 -0
  46. package/src/slash-commands/helpers/parse.ts +2 -1
  47. package/src/tools/bash.ts +9 -0
  48. package/src/tools/composer-bash-policy.ts +96 -0
  49. package/src/tools/fetch.ts +18 -2
  50. package/src/web/insane/url-guard.ts +18 -14
  51. package/src/web/scrapers/types.ts +143 -45
  52. package/src/web/scrapers/utils.ts +70 -19
@@ -65,15 +65,17 @@ function isKnownUltragoalObjective(currentObjective: string): boolean {
65
65
  );
66
66
  }
67
67
 
68
- async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
68
+ async function ultragoalReadPaths(cwd: string): Promise<{ paths: UltragoalPaths; sessionId: string | null }> {
69
69
  const envSessionId = process.env.GJC_SESSION_ID?.trim();
70
- if (envSessionId) return getUltragoalPaths(cwd, envSessionId);
70
+ if (envSessionId) return { paths: getUltragoalPaths(cwd, envSessionId), sessionId: envSessionId };
71
71
  try {
72
72
  const session = await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
73
- return getUltragoalPaths(cwd, session.gjcSessionId);
73
+ return { paths: getUltragoalPaths(cwd, session.gjcSessionId), sessionId: session.gjcSessionId };
74
74
  } catch (error) {
75
75
  if (error instanceof SessionResolutionError && error.code === "no_session") {
76
- return getUltragoalPaths(cwd, null);
76
+ // No session could be resolved (no env, no auto-detectable active session).
77
+ // Surface the null session id so callers can decide; ask-guard treats it as inactive.
78
+ return { paths: getUltragoalPaths(cwd, null), sessionId: null };
77
79
  }
78
80
  throw error;
79
81
  }
@@ -82,7 +84,7 @@ async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
82
84
  async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
83
85
  let paths: UltragoalPaths;
84
86
  try {
85
- paths = await ultragoalReadPaths(cwd);
87
+ ({ paths } = await ultragoalReadPaths(cwd));
86
88
  } catch (error) {
87
89
  if (error instanceof SessionResolutionError) return true;
88
90
  throw error;
@@ -368,14 +370,26 @@ export async function readUltragoalVerificationState(input: {
368
370
 
369
371
  export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
370
372
  let paths: UltragoalPaths;
373
+ let sessionId: string | null;
371
374
  try {
372
- paths = await ultragoalReadPaths(cwd);
375
+ ({ paths, sessionId } = await ultragoalReadPaths(cwd));
373
376
  } catch (error) {
374
377
  return activeAskDiagnostic({
375
378
  reason: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
376
379
  source: "durable_state_unreadable",
377
380
  });
378
381
  }
382
+ // Ultragoal state is session-scoped. When no session can be resolved (no env,
383
+ // no auto-detectable active session) there is no active run to protect, so the
384
+ // ask guard must fall open rather than block on legacy/global durable state.
385
+ if (sessionId === null) {
386
+ return inactiveAskDiagnostic({
387
+ reason: "No active GJC session resolved; ultragoal is inactive.",
388
+ source: "absent",
389
+ goalsPath: paths.goalsPath,
390
+ ledgerPath: paths.ledgerPath,
391
+ });
392
+ }
379
393
  try {
380
394
  await fs.stat(paths.dir);
381
395
  } catch (error) {
@@ -398,8 +412,8 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
398
412
  let plan: UltragoalPlan | null;
399
413
  let ledger: UltragoalLedgerEvent[];
400
414
  try {
401
- plan = await readUltragoalPlan(cwd);
402
- ledger = await readUltragoalLedger(cwd);
415
+ plan = await readUltragoalPlan(cwd, sessionId);
416
+ ledger = await readUltragoalLedger(cwd, sessionId);
403
417
  } catch (error) {
404
418
  return activeAskDiagnostic({
405
419
  reason: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
@@ -409,6 +423,9 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
409
423
  });
410
424
  }
411
425
  if (!plan) {
426
+ // goals.json absent or empty while the state dir exists is an inconsistent
427
+ // durable state, not a clean "no run". Fail closed so the pause guard (which
428
+ // relies on this `durable_state_unreadable` signal) keeps blocking give-ups.
412
429
  return activeAskDiagnostic({
413
430
  reason: "Durable .gjc/ultragoal state exists but goals.json is missing or empty.",
414
431
  source: "durable_state_unreadable",
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { logger } from "@gajae-code/utils";
4
4
  import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
5
+ import { detectDeepInterviewPlaintextAskLeak } from "../deep-interview/plaintext-gate-guard";
5
6
  import { activeSnapshotPath, modeStatePath as sessionModeStatePath } from "../gjc-runtime/session-layout";
6
7
  import { resolveGjcSessionForRead } from "../gjc-runtime/session-resolution";
7
8
  import { ModeStateSchema, SkillActiveStateSchema } from "../gjc-runtime/state-schema";
@@ -675,6 +676,52 @@ async function readCurrentGoalObjectiveFromSessionFile(sessionFile: string | und
675
676
  return typeof objective === "string" && objective.trim().length > 0 ? objective.trim() : null;
676
677
  }
677
678
 
679
+ async function readLatestAssistantTextFromSessionFile(sessionFile: string | undefined): Promise<string | null> {
680
+ const trimmed = sessionFile?.trim();
681
+ if (!trimmed) return null;
682
+ let entries: SessionEntry[];
683
+ try {
684
+ entries = (await loadEntriesFromFile(trimmed)).filter((entry): entry is SessionEntry => entry.type !== "session");
685
+ } catch {
686
+ return null;
687
+ }
688
+ if (entries.length === 0) return null;
689
+ const context = buildSessionContext(entries);
690
+ for (let index = context.messages.length - 1; index >= 0; index--) {
691
+ const message = context.messages[index];
692
+ if (message?.role !== "assistant") continue;
693
+ const text = message.content
694
+ .filter(block => block.type === "text")
695
+ .map(block => block.text)
696
+ .join("");
697
+ const trimmedText = text.trim();
698
+ return trimmedText.length > 0 ? trimmedText : null;
699
+ }
700
+ return null;
701
+ }
702
+
703
+ async function shouldRescueDeepInterviewPlaintextAskLeak(
704
+ skill: GjcWorkflowSkill,
705
+ state: ModeState | null,
706
+ cwd: string,
707
+ sessionFile: string | undefined,
708
+ ): Promise<boolean> {
709
+ if (skill !== "deep-interview") return false;
710
+ if (state?.active !== true) return false;
711
+ const phase = String(state.current_phase ?? "")
712
+ .trim()
713
+ .toLowerCase();
714
+ if (DEEP_INTERVIEW_ABORT_PHASES.has(phase)) return false;
715
+ if (await deepInterviewSpecCrystallized(state, cwd)) return false;
716
+ const latestAssistantText = await readLatestAssistantTextFromSessionFile(sessionFile);
717
+ if (!latestAssistantText) return false;
718
+ return detectDeepInterviewPlaintextAskLeak(latestAssistantText) !== null;
719
+ }
720
+
721
+ function buildDeepInterviewPlaintextAskLeakMessage(statePath: string): string {
722
+ return `GJC deep-interview emitted a Deep Interview question/options block as plain text (${statePath}). It must not wait for a prose answer. Continue immediately by calling the ask tool with the Restate gate question and options: Yes, crystallize; Adjust wording; Missing scope; plus free text/custom input.`;
723
+ }
724
+
678
725
  export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitStateInput): Promise<string | null> {
679
726
  const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
680
727
  const visibleModeState = await readVisibleModeState(input.cwd, "ultragoal", resolvedSessionId, input.stateDir);
@@ -756,6 +803,16 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
756
803
  systemMessage: recoveryMessage,
757
804
  };
758
805
  }
806
+ if (await shouldRescueDeepInterviewPlaintextAskLeak(entry.skill, modeState, input.cwd, input.sessionFile)) {
807
+ const statePath = modeStatePath(input.cwd, entry.skill, resolvedSessionId);
808
+ const rescueMessage = buildDeepInterviewPlaintextAskLeakMessage(statePath);
809
+ return {
810
+ decision: "block",
811
+ reason: rescueMessage,
812
+ stopReason: "gjc_skill_deep_interview_plaintext_ask_leak",
813
+ systemMessage: rescueMessage,
814
+ };
815
+ }
759
816
  if (modeStateReleasesStop(modeState, handoffRequired)) {
760
817
  // A mode-state that claims it releases the Stop block must agree with
761
818
  // authoritative durable state. If a stale/incoherent mode-state would