@looplia/looplia-cli 0.7.3 → 0.7.4

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.
@@ -107,7 +107,7 @@ import {
107
107
  unknownType,
108
108
  util,
109
109
  voidType
110
- } from "./chunk-MHR5TPHE.js";
110
+ } from "./chunk-PXCY2LDE.js";
111
111
  import {
112
112
  pathExists
113
113
  } from "./chunk-VRBGWKZ6.js";
@@ -708,6 +708,66 @@ function parseWorkflow(content) {
708
708
  instructions: body
709
709
  };
710
710
  }
711
+ function generateValidationManifest(definition) {
712
+ const steps = {};
713
+ for (const step of definition.steps) {
714
+ steps[step.id] = {
715
+ output: step.output,
716
+ validate: step.validate,
717
+ validated: false
718
+ };
719
+ }
720
+ return {
721
+ workflow: definition.name,
722
+ version: definition.version,
723
+ finalStepId: getFinalStep(definition),
724
+ steps
725
+ };
726
+ }
727
+ function getExecutionOrder(definition) {
728
+ const stepsMap = new Map(definition.steps.map((s) => [s.id, s]));
729
+ const order = [];
730
+ const visited = /* @__PURE__ */ new Set();
731
+ const visiting = /* @__PURE__ */ new Set();
732
+ function visit(id) {
733
+ if (visited.has(id)) {
734
+ return;
735
+ }
736
+ if (visiting.has(id)) {
737
+ throw new Error(`Circular dependency detected: ${id}`);
738
+ }
739
+ visiting.add(id);
740
+ const step = stepsMap.get(id);
741
+ if (step?.needs) {
742
+ for (const dep of step.needs) {
743
+ if (!stepsMap.has(dep)) {
744
+ throw new Error(`Unknown dependency '${dep}' in step '${id}'`);
745
+ }
746
+ visit(dep);
747
+ }
748
+ }
749
+ visiting.delete(id);
750
+ visited.add(id);
751
+ order.push(id);
752
+ }
753
+ for (const step of definition.steps) {
754
+ visit(step.id);
755
+ }
756
+ return order;
757
+ }
758
+ function getFinalStep(definition) {
759
+ for (const step of definition.steps) {
760
+ if (step.final) {
761
+ return step.id;
762
+ }
763
+ }
764
+ const order = getExecutionOrder(definition);
765
+ const lastStep = order.at(-1);
766
+ if (!lastStep) {
767
+ throw new Error("Workflow has no steps");
768
+ }
769
+ return lastStep;
770
+ }
711
771
  function isInputlessWorkflow(definition) {
712
772
  if (definition.inputs?.some((input) => input.required)) {
713
773
  return false;
@@ -810,6 +870,7 @@ var ValidationManifestSchema = external_exports.object({
810
870
  version: external_exports.string().optional(),
811
871
  sandboxId: external_exports.string().optional(),
812
872
  createdAt: external_exports.string().optional(),
873
+ finalStepId: external_exports.string().optional(),
813
874
  steps: external_exports.record(external_exports.string(), StepValidationStateSchema)
814
875
  });
815
876
  var ValidationCheckSchema = external_exports.object({
@@ -1131,6 +1192,8 @@ async function removeSkill(skillName, registry) {
1131
1192
 
1132
1193
  export {
1133
1194
  parseWorkflow,
1195
+ generateValidationManifest,
1196
+ getFinalStep,
1134
1197
  isInputlessWorkflow,
1135
1198
  extractWorkflowSkills,
1136
1199
  validateUserProfile,
@@ -10,6 +10,7 @@ import {
10
10
  clearClaudeCodePathCache,
11
11
  createClaudeAgentExecutor,
12
12
  createQueryLogger,
13
+ createWorkflowHooks,
13
14
  ensureWorkspace,
14
15
  executeAgenticQueryStreaming,
15
16
  executeInteractiveQueryStreaming,
@@ -31,7 +32,7 @@ import {
31
32
  validateConfig,
32
33
  writeLoopliaSettings,
33
34
  writeUserProfile
34
- } from "./chunk-MHR5TPHE.js";
35
+ } from "./chunk-PXCY2LDE.js";
35
36
  import "./chunk-VRBGWKZ6.js";
36
37
  import "./chunk-Y55L47HC.js";
37
38
  export {
@@ -44,6 +45,7 @@ export {
44
45
  clearClaudeCodePathCache,
45
46
  createClaudeAgentExecutor,
46
47
  createQueryLogger,
48
+ createWorkflowHooks,
47
49
  ensureWorkspace,
48
50
  executeAgenticQueryStreaming,
49
51
  executeInteractiveQueryStreaming,
package/dist/cli.js CHANGED
@@ -3,7 +3,9 @@ import {
3
3
  ensureWorkflowSkills,
4
4
  extractWorkflowSkills,
5
5
  findSkill,
6
+ generateValidationManifest,
6
7
  getAvailableSkills,
8
+ getFinalStep,
7
9
  getInstalledSkills,
8
10
  installSkill,
9
11
  isInputlessWorkflow,
@@ -12,7 +14,7 @@ import {
12
14
  removeSkill,
13
15
  updateSkill,
14
16
  validateUserProfile
15
- } from "./chunk-VYGRYFSY.js";
17
+ } from "./chunk-XKLZXCWO.js";
16
18
  import "./chunk-QQGRKUSM.js";
17
19
  import {
18
20
  addSource,
@@ -29,6 +31,7 @@ import {
29
31
  applyPreset,
30
32
  copyPlugins,
31
33
  createClaudeAgentExecutor,
34
+ createWorkflowHooks,
32
35
  downloadRemotePlugins,
33
36
  ensureWorkspace,
34
37
  getConfigPath,
@@ -42,7 +45,7 @@ import {
42
45
  removeLoopliaSettings,
43
46
  writeLoopliaSettings,
44
47
  writeUserProfile
45
- } from "./chunk-MHR5TPHE.js";
48
+ } from "./chunk-PXCY2LDE.js";
46
49
  import "./chunk-VRBGWKZ6.js";
47
50
  import {
48
51
  copyOutputsToDestination,
@@ -32868,7 +32871,7 @@ async function* analyzeDescriptionStreaming(description, workspace, questionCall
32868
32871
  const { createSandboxDirectories: createSandboxDirectories2, generateSandboxId: generateSandboxId2 } = await import("./sandbox-XVMNWAJO.js");
32869
32872
  const sandboxId = generateSandboxId2("build");
32870
32873
  createSandboxDirectories2(workspace, sandboxId);
32871
- const { executeInteractiveQueryStreaming } = await import("./claude-agent-sdk-SQ6YU4VE.js");
32874
+ const { executeInteractiveQueryStreaming } = await import("./claude-agent-sdk-IC25DTKL.js");
32872
32875
  const prompt = `${buildAnalysisPrompt(description)} --sandbox-id ${sandboxId}`;
32873
32876
  const generator = executeInteractiveQueryStreaming(
32874
32877
  prompt,
@@ -34135,7 +34138,7 @@ async function* executeInteractiveStreamingBatch(prompt, workspace, questionCall
34135
34138
  const sandboxId = generateSandboxId2("build");
34136
34139
  createSandboxDirectories2(workspace, sandboxId);
34137
34140
  const promptWithSandbox = `${prompt} --sandbox-id ${sandboxId}`;
34138
- const { executeInteractiveQueryStreaming } = await import("./claude-agent-sdk-SQ6YU4VE.js");
34141
+ const { executeInteractiveQueryStreaming } = await import("./claude-agent-sdk-IC25DTKL.js");
34139
34142
  const schema = {
34140
34143
  type: "object",
34141
34144
  properties: {
@@ -34456,8 +34459,12 @@ Usage:
34456
34459
  Available presets:
34457
34460
  ANTHROPIC_CLAUDE_HAIKU Anthropic Claude Haiku (default)
34458
34461
  ANTHROPIC_CLAUDE_SONNET Anthropic Claude Sonnet
34462
+ CLAUDE_CODE_SUBSCRIPTION_HAIKU Claude Haiku via subscription (macOS)
34463
+ CLAUDE_CODE_SUBSCRIPTION_SONNET Claude Sonnet via subscription (macOS)
34464
+ CLAUDE_CODE_SUBSCRIPTION_OPUS Claude Opus via subscription (macOS)
34459
34465
  ZENMUX_ANTHROPIC_HAIKU45 ZenMux Claude Haiku 4.5
34460
34466
  ZENMUX_ZAI_GLM47 ZenMux GLM-4.7
34467
+ ZENMUX_ZAI_GLM47FLASHX ZenMux GLM-4.7-FlashX
34461
34468
  ZENMUX_ZAI_GLM46VFLASH ZenMux GLM-4.6v-Flash
34462
34469
  ZENMUX_MINIMAX_M21 ZenMux MiniMax-M2.1
34463
34470
  ZENMUX_GOOGLE_GEMINI3FLASH ZenMux Gemini-3-Flash
@@ -34466,10 +34473,14 @@ Available presets:
34466
34473
  ZENMUX_XAI_GROK41FAST ZenMux Grok-4.1-Fast
34467
34474
  ZENMUX_DEEPSEEK_V32 ZenMux DeepSeek-v3.2
34468
34475
  ZENMUX_MISTRAL_LARGE2512 ZenMux Mistral-Large-2512
34476
+ OPENROUTER_PRESET OpenRouter (user-configured preset)
34477
+ OPENROUTER_ZAI_GLM47FLASH OpenRouter GLM-4.7-Flash
34478
+ OLLAMA_GLM47_CLOUD Ollama GLM-4.7 Cloud
34479
+ OLLAMA_MINIMAX_M21_CLOUD Ollama MiniMax-M2.1 Cloud
34469
34480
 
34470
34481
  Configuration keys for 'set':
34471
- api-provider Provider type: anthropic, zenmux, custom
34472
- base-url API base URL (for zenmux/custom)
34482
+ api-provider Provider type: anthropic, zenmux, openrouter, ollama, custom
34483
+ base-url API base URL (for zenmux/openrouter/ollama/custom)
34473
34484
  auth-token Authentication token (fallback if env var not set)
34474
34485
  main-model Model for main agent
34475
34486
  executor-model Model for skill executor
@@ -34477,9 +34488,13 @@ Configuration keys for 'set':
34477
34488
  API keys (set in .env - looplia auto-maps based on provider):
34478
34489
  ANTHROPIC_API_KEY For Anthropic (direct)
34479
34490
  ZENMUX_API_KEY For ZenMux (auto-mapped to ANTHROPIC_API_KEY)
34491
+ OPENROUTER_API_KEY For OpenRouter (auto-mapped to ANTHROPIC_AUTH_TOKEN)
34492
+ OLLAMA_API_KEY For Ollama (optional, defaults to "ollama")
34480
34493
 
34481
34494
  Examples:
34482
34495
  looplia config provider preset ZENMUX_ZAI_GLM47
34496
+ looplia config provider preset OPENROUTER_PRESET
34497
+ looplia config provider preset OLLAMA_GLM47_CLOUD
34483
34498
  looplia config provider show
34484
34499
  `);
34485
34500
  }
@@ -34605,7 +34620,9 @@ async function showProviderConfig() {
34605
34620
  console.log(` Preset: ${info.preset}`);
34606
34621
  }
34607
34622
  console.log(` Provider: ${info.provider}`);
34608
- if (info.authToken) {
34623
+ if (info.authTokenSource === "subscription") {
34624
+ console.log(" Auth Source: Claude Code Subscription (OAuth)");
34625
+ } else if (info.authToken) {
34609
34626
  console.log(` Auth Token: ${maskAuthToken(info.authToken)}`);
34610
34627
  }
34611
34628
  console.log("\n Agent Models:");
@@ -35366,7 +35383,10 @@ function executeMock2(args) {
35366
35383
  }
35367
35384
  async function executeStreaming(prompt, workspace, workflowId, requiredSkills) {
35368
35385
  const contentId = crypto.randomUUID();
35369
- const executor = createClaudeAgentExecutor({ workspace });
35386
+ const executor = createClaudeAgentExecutor({
35387
+ workspace,
35388
+ runHooks: createWorkflowHooks()
35389
+ });
35370
35390
  const generator = executor.executePromptStreaming(prompt, {
35371
35391
  workspace,
35372
35392
  contentId,
@@ -35394,14 +35414,21 @@ async function executeStreaming(prompt, workspace, workflowId, requiredSkills) {
35394
35414
  async function executeBatch2(prompt, workspace, workflowId, requiredSkills) {
35395
35415
  console.error("\u23F3 Processing...");
35396
35416
  const contentId = crypto.randomUUID();
35397
- const executor = createClaudeAgentExecutor({ workspace });
35417
+ const executor = createClaudeAgentExecutor({
35418
+ workspace,
35419
+ runHooks: createWorkflowHooks()
35420
+ });
35398
35421
  const result = await executor.executePrompt(prompt, {
35399
35422
  workspace,
35400
35423
  contentId,
35401
35424
  requiredSkills
35402
35425
  });
35403
- if (result.success && result.data) {
35404
- return result.data;
35426
+ if (result.success) {
35427
+ return result.data ?? {
35428
+ status: "success",
35429
+ workflowId,
35430
+ sessionId: result.sessionId
35431
+ };
35405
35432
  }
35406
35433
  return {
35407
35434
  status: "error",
@@ -35508,6 +35535,27 @@ async function handleJitInstallation(workflowInfo) {
35508
35535
  return true;
35509
35536
  }
35510
35537
  }
35538
+ function populateValidationManifest(workspace, sandboxId, workflowId, workflowInfo) {
35539
+ if (!workflowInfo?.parsed?.definition) {
35540
+ return;
35541
+ }
35542
+ const manifest = generateValidationManifest(workflowInfo.parsed.definition);
35543
+ const finalStepId = getFinalStep(workflowInfo.parsed.definition);
35544
+ const sandboxDir = join3(workspace, "sandbox", sandboxId);
35545
+ const validationPath = join3(sandboxDir, "validation.json");
35546
+ if (!existsSync4(validationPath)) {
35547
+ createInitialValidationJson(sandboxDir, sandboxId, workflowId);
35548
+ }
35549
+ if (!existsSync4(validationPath)) {
35550
+ return;
35551
+ }
35552
+ const existing = JSON.parse(
35553
+ readFileSync2(validationPath, "utf-8")
35554
+ );
35555
+ existing.steps = manifest.steps;
35556
+ existing.finalStepId = finalStepId;
35557
+ writeFileSync2(validationPath, JSON.stringify(existing, null, 2), "utf-8");
35558
+ }
35511
35559
  async function runRunCommand(args) {
35512
35560
  const parsed = parseArgs3(args);
35513
35561
  if (parsed.help) {
@@ -35524,6 +35572,14 @@ async function runRunCommand(args) {
35524
35572
  const workflowInfo = parseWorkflowFile(workspace, parsed.workflowId);
35525
35573
  const allowInputless = workflowInfo?.isInputless ?? false;
35526
35574
  const sandboxId = resolveSandboxId(workspace, parsed, allowInputless);
35575
+ process.env.LOOPLIA_SANDBOX_ID = sandboxId;
35576
+ process.env.LOOPLIA_SANDBOX_ROOT = join3(workspace, "sandbox");
35577
+ populateValidationManifest(
35578
+ workspace,
35579
+ sandboxId,
35580
+ parsed.workflowId,
35581
+ workflowInfo
35582
+ );
35527
35583
  if (!(parsed.mock || await handleJitInstallation(workflowInfo))) {
35528
35584
  process.exit(1);
35529
35585
  }
@@ -35622,7 +35678,7 @@ async function skillAdd(nameOrUrl, from) {
35622
35678
  }
35623
35679
  if (GITHUB_URL_PATTERN.test(nameOrUrl)) {
35624
35680
  console.log(`Installing skill from URL: ${nameOrUrl}...`);
35625
- const { installSkillFromUrl } = await import("./dist-PMEIK6PJ.js");
35681
+ const { installSkillFromUrl } = await import("./dist-LKL7WJ7K.js");
35626
35682
  const result2 = await installSkillFromUrl(nameOrUrl, true);
35627
35683
  switch (result2.status) {
35628
35684
  case "installed":
@@ -35859,7 +35915,7 @@ async function runSkillCommand(args) {
35859
35915
  }
35860
35916
 
35861
35917
  // src/index.ts
35862
- var VERSION = "0.7.3";
35918
+ var VERSION = "0.7.4";
35863
35919
  function printHelp5() {
35864
35920
  console.log(`
35865
35921
  looplia - Content intelligence CLI (v${VERSION})
@@ -11,7 +11,7 @@ import {
11
11
  loadCompiledRegistry,
12
12
  removeSkill,
13
13
  updateSkill
14
- } from "./chunk-VYGRYFSY.js";
14
+ } from "./chunk-XKLZXCWO.js";
15
15
  import {
16
16
  syncRegistrySources,
17
17
  syncSource
@@ -32,7 +32,7 @@ import {
32
32
  CORE_SKILLS,
33
33
  ensureWorkspace,
34
34
  isCoreSkill
35
- } from "./chunk-MHR5TPHE.js";
35
+ } from "./chunk-PXCY2LDE.js";
36
36
  import "./chunk-VRBGWKZ6.js";
37
37
  import "./chunk-Y55L47HC.js";
38
38
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looplia/looplia-cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Looplia CLI - AI-powered workflow automation tool",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -39,9 +39,7 @@ steps:
39
39
  Include meta information (difficulty, time to write, audience).
40
40
  Calculate relevance scores based on user profile.
41
41
  needs: [summary, ideas]
42
- input:
43
- - ${{ steps.summary.output }}
44
- - ${{ steps.ideas.output }}
42
+ input: ["${{ steps.summary.output }}", "${{ steps.ideas.output }}"]
45
43
  output: ${{ sandbox }}/outputs/writing-kit.json
46
44
  final: true
47
45
  validate:
@@ -1,22 +0,0 @@
1
- {
2
- "$schema": "https://code.claude.com/schemas/hooks.json",
3
- "hooks": [
4
- {
5
- "event": "PostToolUse",
6
- "matcher": "Write",
7
- "command": "./scripts/hooks/post-write-validate.sh",
8
- "description": "Auto-validate artifacts written to sandbox outputs"
9
- },
10
- {
11
- "event": "Stop",
12
- "command": "./scripts/hooks/stop-guard.sh",
13
- "description": "Guard workflow completion - block until all validated"
14
- },
15
- {
16
- "event": "SessionStart",
17
- "matcher": "compact",
18
- "command": "./scripts/hooks/compact-inject-state.sh",
19
- "description": "Re-inject sandbox state after context compact"
20
- }
21
- ]
22
- }
@@ -1,36 +0,0 @@
1
- #!/bin/bash
2
- # Context Compact State Injection Hook (v0.6.0)
3
- # Triggered: When context is compacted (SessionStart:compact)
4
- # Action: Re-inject current sandbox progress into new context
5
-
6
- set -euo pipefail
7
-
8
- # Find active sandbox
9
- SANDBOX_BASE="${HOME}/.looplia/sandbox"
10
- if [[ ! -d "$SANDBOX_BASE" ]]; then
11
- exit 0
12
- fi
13
-
14
- # Sort by modification time (newest first) to get most recent sandbox
15
- SANDBOX_DIR=$(ls -td "$SANDBOX_BASE"/*/ 2>/dev/null | head -1 | sed 's:/$::')
16
- if [[ -z "$SANDBOX_DIR" ]]; then
17
- exit 0
18
- fi
19
-
20
- VALIDATION_JSON="$SANDBOX_DIR/validation.json"
21
- SANDBOX_ID=$(basename "$SANDBOX_DIR")
22
-
23
- if [[ ! -f "$VALIDATION_JSON" ]]; then
24
- exit 0
25
- fi
26
-
27
- WORKFLOW=$(jq -r '.workflow // "unknown"' "$VALIDATION_JSON")
28
-
29
- # Build progress summary (v0.6.0 uses "steps" not "outputs")
30
- echo "=== Active Sandbox: $SANDBOX_ID ==="
31
- echo "Workflow: $WORKFLOW"
32
- echo ""
33
- echo "Progress:"
34
- jq -r '.steps | to_entries[] | " - \(.key): \(if .value.validated then "✓ validated" else "⏳ pending" end)"' "$VALIDATION_JSON"
35
- echo ""
36
- echo "Next: Complete pending steps in dependency order."
@@ -1,81 +0,0 @@
1
- #!/bin/bash
2
- # Post-Write Artifact Validator Hook (v0.6.0)
3
- # Triggered: When Write tool completes
4
- # Action: Run semantic validation via validate.ts for sandbox outputs
5
-
6
- set -euo pipefail
7
-
8
- # Read JSON input from stdin
9
- INPUT=$(cat)
10
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
11
-
12
- # Only process sandbox/ files (matches sandbox/{id}/outputs/*.json pattern)
13
- if [[ "$FILE_PATH" != *"/sandbox/"* ]]; then
14
- exit 0
15
- fi
16
-
17
- # Check if it's in outputs/ directory
18
- if [[ "$FILE_PATH" != *"/outputs/"* ]]; then
19
- exit 0
20
- fi
21
-
22
- # Extract sandbox directory and artifact name
23
- SANDBOX_DIR=$(dirname "$(dirname "$FILE_PATH")")
24
- ARTIFACT=$(basename "$FILE_PATH" .json)
25
- VALIDATION_JSON="$SANDBOX_DIR/validation.json"
26
-
27
- # Check for validation.json
28
- if [[ ! -f "$VALIDATION_JSON" ]]; then
29
- exit 0
30
- fi
31
-
32
- # Use file locking to prevent race conditions when multiple writes happen concurrently
33
- # flock ensures exclusive access to validation.json during read-modify-write cycle
34
- LOCK_FILE="${VALIDATION_JSON}.lock"
35
-
36
- (
37
- # Acquire exclusive lock (wait up to 30 seconds)
38
- if ! flock -x -w 30 200; then
39
- echo "Failed to acquire lock on validation.json" >&2
40
- exit 1
41
- fi
42
-
43
- # Get validation criteria from validation.json (v0.6.0 uses "steps" not "outputs")
44
- CRITERIA=$(jq -r --arg art "$ARTIFACT" '.steps[$art].validate // empty' "$VALIDATION_JSON" 2>/dev/null)
45
-
46
- if [[ -z "$CRITERIA" || "$CRITERIA" == "null" ]]; then
47
- # No criteria defined - just check JSON validity
48
- if ! jq empty "$FILE_PATH" 2>/dev/null; then
49
- echo "Validation failed for $ARTIFACT: Invalid JSON" >&2
50
- exit 2
51
- fi
52
- else
53
- # Run full semantic validation via validate.ts
54
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
55
- VALIDATOR_SCRIPT="$SCRIPT_DIR/../../skills/workflow-validator/scripts/validate.ts"
56
-
57
- if [[ -f "$VALIDATOR_SCRIPT" ]]; then
58
- # Run the validation script
59
- RESULT=$(bun "$VALIDATOR_SCRIPT" "$FILE_PATH" "$CRITERIA" 2>&1) || true
60
- PASSED=$(echo "$RESULT" | jq -r '.passed // false' 2>/dev/null) || PASSED="false"
61
-
62
- if [[ "$PASSED" != "true" ]]; then
63
- echo "Semantic validation failed for $ARTIFACT:" >&2
64
- echo "$RESULT" >&2
65
- exit 2
66
- fi
67
- else
68
- # Fallback to basic JSON check if validator script not found
69
- if ! jq empty "$FILE_PATH" 2>/dev/null; then
70
- echo "Validation failed for $ARTIFACT: Invalid JSON" >&2
71
- exit 2
72
- fi
73
- fi
74
- fi
75
-
76
- # Update validation.json to mark step as validated (v0.6.0 uses "steps")
77
- jq --arg art "$ARTIFACT" '.steps[$art].validated = true' "$VALIDATION_JSON" > "${VALIDATION_JSON}.tmp"
78
- mv "${VALIDATION_JSON}.tmp" "$VALIDATION_JSON"
79
- echo "✓ Validated: $ARTIFACT.json" >&2
80
-
81
- ) 200>"$LOCK_FILE"
@@ -1,56 +0,0 @@
1
- #!/bin/bash
2
- # Workflow Completion Guard Hook (v0.6.1)
3
- # Triggered: When main agent attempts to stop
4
- # Action: Block if any step has validated: false OR output files missing
5
-
6
- set -euo pipefail
7
-
8
- INPUT=$(cat)
9
- STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
10
-
11
- # Prevent infinite loop
12
- if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then
13
- exit 0
14
- fi
15
-
16
- # Find active sandbox (most recently modified sandbox directory)
17
- SANDBOX_BASE="${HOME}/.looplia/sandbox"
18
- if [[ ! -d "$SANDBOX_BASE" ]]; then
19
- exit 0
20
- fi
21
-
22
- # Sort by modification time (newest first) to get most recent sandbox
23
- SANDBOX_DIR=$(ls -td "$SANDBOX_BASE"/*/ 2>/dev/null | head -1 | sed 's:/$::')
24
- if [[ -z "$SANDBOX_DIR" ]]; then
25
- exit 0
26
- fi
27
-
28
- VALIDATION_JSON="$SANDBOX_DIR/validation.json"
29
- if [[ ! -f "$VALIDATION_JSON" ]]; then
30
- exit 0
31
- fi
32
-
33
- # Check for missing output files first (more actionable feedback)
34
- MISSING=""
35
- for step in $(jq -r '.steps | keys[]' "$VALIDATION_JSON" 2>/dev/null); do
36
- OUTPUT_PATH=$(jq -r --arg s "$step" '.steps[$s].output // empty' "$VALIDATION_JSON" 2>/dev/null)
37
- if [[ -n "$OUTPUT_PATH" && ! -f "$OUTPUT_PATH" ]]; then
38
- MISSING="$MISSING $step"
39
- fi
40
- done
41
-
42
- if [[ -n "$MISSING" ]]; then
43
- echo "{\"decision\": \"block\", \"reason\": \"Missing output files for steps:$MISSING. You MUST call the Write tool to create these files at the paths specified in the workflow.\"}"
44
- exit 0
45
- fi
46
-
47
- # Check all steps are validated (v0.6.0 uses "steps" not "outputs")
48
- PENDING=$(jq -r '.steps | to_entries[] | select(.value.validated == false) | .key' "$VALIDATION_JSON" 2>/dev/null | tr '\n' ', ' | sed 's/,$//')
49
-
50
- if [[ -n "$PENDING" ]]; then
51
- echo "{\"decision\": \"block\", \"reason\": \"Workflow incomplete. Pending validation for steps: $PENDING. Output files exist but need validation - re-write them to trigger validation.\"}"
52
- exit 0
53
- fi
54
-
55
- # All validated - allow stop
56
- exit 0