Skip to content
Development
Hook

Hooks

What devpace runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
devpace
7320 skills3 agents9 hooks
Install
> /plugin marketplace add arch-team/devpace
> /plugin install devpace@devpace

Ships with devpace. Installing the plugin gets these hooks.

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh

PreToolUse

  • MatchesWrite|Edit${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs

PostToolUse

  • MatchesWrite|Edit${CLAUDE_PLUGIN_ROOT}/hooks/post-cr-update.mjs${CLAUDE_PLUGIN_ROOT}/hooks/pulse-counter.mjs${CLAUDE_PLUGIN_ROOT}/hooks/sync-push.mjs${CLAUDE_PLUGIN_ROOT}/hooks/post-schema-check.mjs

PostToolUseFailure

  • MatchesWrite|Edit${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-failure.mjs

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • ${CLAUDE_PLUGIN_ROOT}/hooks/skill-eval.mjs

PreCompact

  • ${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.sh

Stop

  • ${CLAUDE_PLUGIN_ROOT}/hooks/session-stop.sh

SessionEnd

  • ${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh

SubagentStop

  • ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.mjs
Read hooks/hooks.json

Where it lives

  • hooks/post-cr-update.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * devpace PostToolUse hook — detect CR state transitions and trigger pipelines
     *
     * Purpose: After a Write/Edit to a CR file, detect state transitions and output
     * appropriate ACTION signals:
     * - merged → post-merge pipeline (§11)
     * - created → auto-link/create suggestion (sync-procedures-auto.md)
     * - gate failures / rejections → learning triggers
     *
     * This is an advisory hook (exit 0), not blocking.
     */
    
    import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
    import { basename, dirname } from 'node:path';
    import { readStdinJson, getProjectDir, extractFilePath, isCrFile, readCrState, getLastEvent, CR_STATES } from './lib/utils.mjs';
    
    const input = await readStdinJson();
    const projectDir = getProjectDir();
    const backlogDir = `${projectDir}/.devpace/backlog`;
    
    // Only act if .devpace exists and has backlog
    if (!existsSync(backlogDir)) {
      process.exit(0);
    }
    
    // Extract file path from tool input
    const filePath = extractFilePath(input);
    
    // Only check writes to CR files
    if (!isCrFile(filePath, backlogDir)) {
      process.exit(0);
    }
    
    // Check CR state and recent events for learning triggers
    if (existsSync(filePath)) {
      let content;
      try {
        content = readFileSync(filePath, 'utf-8');
      } catch {
        process.exit(0);
      }
    
      const currentState = readCrState(filePath, content);
      const crName = basename(filePath, '.md');
    
      if (currentState === CR_STATES.MERGED) {
        console.log(`devpace:post-merge ${crName} 已 merge。ACTION: 执行 post-merge 管线——1) 更新 state.md 移除该 CR 的进行中标记 2) 若 CR 有外部关联则执行 /pace-sync 同步状态到外部(关闭 Issue + 完成标签) 3) 执行 /pace-learn merge ${crName} 萃取经验。`);
      }
    
      // Auto-link/create trigger — new CR created with sync configured
      // Uses a marker file to avoid re-suggesting on subsequent edits to the same CR
      if (currentState === CR_STATES.CREATED) {
        const syncMappingPath = `${projectDir}/.devpace/integrations/sync-mapping.md`;
        const hasExternalLink = /\*\*外部关联\*\*[::]\s*\[/.test(content);
        const autoSuggestMark = `${projectDir}/.devpace/.sync-auto-suggested-${crName}`;
        if (existsSync(syncMappingPath) && !hasExternalLink && !existsSync(autoSuggestMark)) {
          try {
            mkdirSync(dirname(autoSuggestMark), { recursive: true });
            writeFileSync(autoSuggestMark, new Date().toISOString(), 'utf-8');
          } catch { /* non-critical */ }
          console.log(`devpace:auto-sync ${crName} 已创建且同步已配置。ACTION: 加载 sync-procedures-auto.md,为 ${crName} 执行自动关联/创建外部 Issue 流程(§2 auto-link 优先,无匹配则 §3 auto-create)。`);
        }
      }
    
      // Gate fail learning trigger — gate_fail is a valuable learning opportunity
      const recentEvent = getLastEvent(filePath, content);
      if (recentEvent && (recentEvent.type === 'gate1_fail' || recentEvent.type === 'gate2_fail')) {
        const gateNum = recentEvent.type === 'gate1_fail' ? '1' : '2';
        console.log(`devpace:learn-trigger ${crName} Gate ${gateNum} 未通过。ACTION: 先修复 Gate 失败原因并重试;Gate 通过后执行 /pace-learn gate-failure ${crName} 萃取教训。`);
      }
    
      // Rejected learning trigger — human rejection reveals understanding gaps
      if (recentEvent && recentEvent.type === 'rejected') {
        console.log(`devpace:learn-trigger ${crName} 被人类驳回。ACTION: 查看 CR 事件表最新 rejected 记录确认驳回原因,修复后重新提交 review;完成后执行 /pace-learn rejection ${crName} 分析认知差距。`);
      }
    
      // Gate result sync trigger — push Gate results to external platform
      if (recentEvent && /^gate[123]_(pass|fail)$/.test(recentEvent.type)) {
        const syncMappingPath = `${projectDir}/.devpace/integrations/sync-mapping.md`;
        const hasExternalLink = /\*\*外部关联\*\*[::]\s*\[/.test(content);
        if (existsSync(syncMappingPath) && hasExternalLink) {
          const gateMatch = recentEvent.type.match(/^gate(\d)_(pass|fail)$/);
          const gateNum = gateMatch[1];
          const gateResult = gateMatch[2] === 'pass' ? '通过' : '未通过';
          console.log(`devpace:gate-sync ${crName} Gate ${gateNum} ${gateResult}。ACTION: 加载 sync-procedures-push-advanced.md §2,为 ${crName} 推送 Gate ${gateNum} 结果到外部平台(Comment + 标签)。`);
        }
      }
    }
    
    process.exit(0);
    
  • hooks/post-schema-check.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * PostToolUse hook — automatic Schema validation after .devpace/ file writes.
     *
     * Advisory only (exit 0) — outputs warnings/errors but never blocks.
     * Runs validate-schema.mjs on the written file if it's a validatable type
     * (CR, state, project, PF, BR).
     *
     * Exit codes:
     *   0 = always (advisory, non-blocking)
     */
    
    import { existsSync } from 'node:fs';
    import { basename, dirname, join } from 'node:path';
    import { fileURLToPath } from 'node:url';
    import { execFileSync } from 'node:child_process';
    import { readStdinJson, getProjectDir, extractFilePath, isDevpaceFile } from './lib/utils.mjs';
    
    const input = await readStdinJson();
    const projectDir = getProjectDir();
    const filePath = extractFilePath(input);
    
    // Only check .devpace/ files
    if (!filePath || !isDevpaceFile(filePath)) {
      process.exit(0);
    }
    
    // Only check validatable file types
    const name = basename(filePath);
    const isValidatable =
      name === 'state.md' ||
      name === 'project.md' ||
      /^CR-\d{3}\.md$/.test(name) ||
      /^PF-\d{3}\.md$/.test(name) ||
      /^BR-\d{3}\.md$/.test(name);
    
    if (!isValidatable) {
      process.exit(0);
    }
    
    // Find .devpace directory from file path
    const devpaceMatch = filePath.match(/(.+\/.devpace)\//);
    if (!devpaceMatch) {
      process.exit(0);
    }
    const devpaceDir = devpaceMatch[1];
    
    // Run validation
    try {
      const scriptDir = dirname(fileURLToPath(import.meta.url));
      const scriptPath = join(scriptDir, '..', 'scripts', 'validate-schema.mjs');
    
      if (!existsSync(scriptPath)) {
        process.exit(0); // Script not available, skip silently
      }
    
      let output;
      try {
        output = execFileSync(
          'node', [scriptPath, devpaceDir, '--file', filePath],
          { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
        );
      } catch (execErr) {
        // validate-schema.mjs exits 1 on errors — capture stdout from the error
        output = execErr.stdout || '';
      }
    
      if (!output) { process.exit(0); }
      const result = JSON.parse(output);
      if (!result.valid) {
        const r = result.results[0];
        const issues = [...r.errors.map(e => `error: ${e}`), ...r.warnings.map(w => `warning: ${w}`)];
        const schemaPathMap = {
          'state.md': 'process/state-format',
          'project.md': 'entity/project-format',
        };
        const schemaPath = schemaPathMap[name] || (name.startsWith('CR-') ? 'entity/cr-format' : name.startsWith('PF-') ? 'entity/pf-format' : name.startsWith('BR-') ? 'entity/br-format' : 'unknown');
        console.log(`devpace:schema-check ${name} 校验不通过(${r.errors.length} error, ${r.warnings.length} warning):${issues.slice(0, 3).join('; ')}${issues.length > 3 ? ` (+${issues.length - 3} more)` : ''}. ACTION: 重新读取 ${name},按上述错误逐一修复,修复后重新写入触发再次校验。格式参考:knowledge/_schema/${schemaPath}-format.md。`);
      }
    } catch {
      // Validation failure is non-critical — skip silently
    }
    
    process.exit(0);
    
  • hooks/post-tool-failure.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * devpace PostToolUseFailure hook — detect tool failures in advance mode
     *
     * Purpose: When a Write/Edit tool fails during advance mode, remind Claude
     * to check CR state consistency. Prevents CR state and file content from
     * becoming out of sync after a failed write operation.
     *
     * Advisory hook (exit 0), not blocking.
     */
    
    import { existsSync } from 'node:fs';
    import { readStdinJson, getProjectDir, extractFilePath, isCrFile, isAdvanceMode } from './lib/utils.mjs';
    
    const input = await readStdinJson();
    const projectDir = getProjectDir();
    const backlogDir = `${projectDir}/.devpace/backlog`;
    
    // Only act if .devpace exists and we're in advance mode
    if (!existsSync(backlogDir) || !isAdvanceMode(projectDir)) {
      process.exit(0);
    }
    
    const filePath = extractFilePath(input);
    
    // Check if the failed write was targeting a CR file
    if (isCrFile(filePath, backlogDir)) {
      console.log(`devpace:tool-failure CR 文件写入失败。ACTION: 1) 读取 CR 文件确认状态字段是否仍为上次成功值 2) 若状态不一致则在事件表补记 write_failed 条目 3) 执行 git diff ${filePath} 检查部分写入,必要时 git checkout -- ${filePath} 恢复。`);
    } else if (filePath && filePath.includes('.devpace/')) {
      console.log(`devpace:tool-failure .devpace/ 文件写入失败。ACTION: 读取 state.md 确认与当前进度一致;若不一致则执行 git checkout -- ${filePath} 恢复后重试写入。`);
    }
    
    process.exit(0);
    
  • hooks/pre-compact.shRunsGitHub
    Read the script
    #!/bin/bash
    # devpace PreCompact hook — save state snapshot and inject recovery context
    #
    # Purpose: Before Claude auto-compacts (context ~95%), output structured
    # recovery context that will survive compression, ensuring continuity.
    # Enhances cross-session continuity (OBJ-1) and long-session stability (UX4).
    #
    # Advisory hook (exit 0), not blocking.
    
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    STATE_FILE="${PROJECT_DIR}/.devpace/state.md"
    DEVPACE_DIR="${PROJECT_DIR}/.devpace"
    
    if [ ! -f "$STATE_FILE" ]; then
      exit 0
    fi
    
    # Extract key context for post-compact recovery
    echo "devpace:pre-compact === DEVPACE RECOVERY CONTEXT (preserve after compact) ==="
    
    # 1. Iron Rules reminder
    echo "devpace:pre-compact IR-1: state.md 是唯一会话锚点 | IR-2: 先读后写 | IR-3: 质量门不可跳过 | IR-4: 变更走流程 | IR-5: 产出可追溯"
    
    # 2. Current state summary (extract from state.md)
    if [ -f "$STATE_FILE" ]; then
      # Extract 进行中 and 下一步 from state.md
      CURRENT=$(grep -m1 "进行中\|developing\|verifying\|in_review" "$STATE_FILE" 2>/dev/null || echo "")
      NEXT_STEP=$(grep -m1 "下一步" "$STATE_FILE" 2>/dev/null || echo "")
      if [ -n "$CURRENT" ]; then
        echo "devpace:pre-compact Current: $CURRENT"
      fi
      if [ -n "$NEXT_STEP" ]; then
        echo "devpace:pre-compact Next: $NEXT_STEP"
      fi
    fi
    
    # 3. Active CR detection + execution snapshot extraction
    if [ -d "${DEVPACE_DIR}/backlog" ]; then
      ACTIVE_CRS=$(grep -rl "developing\|verifying\|in_review" "${DEVPACE_DIR}/backlog/" 2>/dev/null | head -3)
      if [ -n "$ACTIVE_CRS" ]; then
        for cr in $ACTIVE_CRS; do
          [ ! -f "$cr" ] && continue
          CR_NAME=$(basename "$cr" .md)
          CR_STATUS=$(grep -m1 "状态" "$cr" 2>/dev/null | head -1)
          echo "devpace:pre-compact Active CR: $CR_NAME — $CR_STATUS"
    
          # Extract execution snapshot restore hint (L/XL CRs only)
          snapshot=$(sed -n '/^## 执行快照/,/^## /p' "$cr" | head -10)
          if [ -n "$snapshot" ]; then
            restore_hint=$(echo "$snapshot" | grep '恢复建议' | sed 's/.*| //')
            if [ -n "$restore_hint" ]; then
              echo "devpace:pre-compact   快照: $restore_hint"
            fi
          fi
        done
      fi
    fi
    
    echo "devpace:pre-compact ACTION: 1) Read .devpace/state.md to restore full context 2) Resume active CR 3) Git commit any uncommitted changes"
    echo "devpace:pre-compact === END RECOVERY CONTEXT ==="
    
    exit 0
    
  • hooks/pre-tool-use.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * devpace PreToolUse hook — enforcing quality gates and mode constraints
     *
     * Purpose: Enforce devpace iron rules at the mechanism level, not just text-based rules.
     *
     * Enforcement levels:
     *   1. BLOCKING (exit 2): Explore mode state escalation, Gate 3 bypass attempts
     *   2. ADVISORY (exit 0): Gate 1/2 reminders during normal development flow
     *
     * Iron rules enforced:
     *   - Explore mode: block state.md writes and CR state escalation to advance-mode
     *     states (developing/verifying/in_review). Allow other .devpace/ writes so
     *     management Skills (pace-change, pace-biz, pace-plan) can operate.
     *   - Gate 3: human approval required, no automated state change to approved (devpace-rules.md §2)
     */
    
    import { existsSync } from 'node:fs';
    import {
      readStdinJson, getProjectDir, extractFilePath, extractWriteContent,
      isCrFile, readCrState, isDevpaceFile, isAdvanceMode, isStateChangeToApproved,
      isStateEscalation, CR_STATES
    } from './lib/utils.mjs';
    
    const input = await readStdinJson();
    const projectDir = getProjectDir();
    const backlogDir = `${projectDir}/.devpace/backlog`;
    
    // Only act if .devpace exists and has backlog
    if (!existsSync(backlogDir)) {
      process.exit(0);
    }
    
    const filePath = extractFilePath(input);
    
    // ── ENFORCEMENT 1: Explore mode protection ──────────────────────────
    // Narrowed scope: only block high-risk state operations in explore mode.
    // Management Skills (pace-change, pace-biz, pace-plan) need to write to
    // .devpace/ files even without an active CR, so we only block:
    //   1. state.md direct modification — progress state shouldn't change in explore mode
    //   2. CR state escalation — setting developing/verifying/in_review requires advance mode
    if (isDevpaceFile(filePath) && !isAdvanceMode(projectDir)) {
      const isStateMd = filePath.endsWith('/state.md') || filePath.endsWith('/.devpace/state.md');
      const isCrStateEsc = isCrFile(filePath, backlogDir)
        && isStateEscalation(extractWriteContent(input));
    
      if (isStateMd || isCrStateEsc) {
        console.error('devpace:blocked 探索模式禁止修改进度状态(state.md 或 CR 状态升级)。ACTION: 告知用户需要先进入推进模式,引导用户说"帮我实现 X"或"开始做 CR-NNN"以激活 /pace-dev。');
        process.exit(2);
      }
    }
    
    // ── ENFORCEMENT 2 + ADVISORY: Gate checks ───────────────────────────
    // Single read for both Gate 3 enforcement and advisory reminders
    if (isCrFile(filePath, backlogDir) && existsSync(filePath)) {
      const currentState = readCrState(filePath);
    
      // Gate 3: human approval required — in_review → approved blocked
      if (currentState === CR_STATES.IN_REVIEW) {
        const newContent = extractWriteContent(input);
        if (isStateChangeToApproved(newContent)) {
          console.error('devpace:blocked Gate 3 铁律:CR 从 in_review→approved 必须由人类明确批准。ACTION: 向用户展示 review 摘要(diff 概要+验收标准对比),然后询问是否批准该变更,等待用户回复批准/approved后再修改状态。');
          process.exit(2);
        }
      }
    
      // Advisory: Quality gate reminders
      switch (currentState) {
        case CR_STATES.DEVELOPING:
          console.log("devpace:gate-reminder CR 状态 developing。推进到 verifying 前须通过 Gate 1。ACTION: 执行 lint+test+typecheck,全部通过后在 CR 事件表记录 gate1_pass,再将状态改为 verifying。");
          break;
        case CR_STATES.VERIFYING:
          console.log("devpace:gate-reminder CR 状态 verifying。推进到 in_review 前须通过 Gate 2。ACTION: 执行集成测试+意图一致性检查(对比 CR 验收标准与实际实现),通过后在事件表记录 gate2_pass,再将状态改为 in_review。");
          break;
        case CR_STATES.IN_REVIEW:
          console.log("devpace:gate-reminder CR 状态 in_review。Gate 3 须人类批准。ACTION: 向用户展示变更摘要(diff 概要+验收标准对比),等待用户明确说'批准'。");
          break;
      }
    }
    
    process.exit(0);
    
  • hooks/pulse-counter.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * devpace PostToolUse hook — periodic write-count reminder
     *
     * Purpose: Track write operations and periodically remind Claude to check
     * project status. This is a WRITE VOLUME reminder, complementary to but
     * distinct from pace-pulse (rhythm health detection).
     *
     * Coordination with pace-pulse:
     * - pulse-counter: triggers every 10 writes → suggests /pace-status (write volume)
     * - pace-pulse: triggers every 5 checkpoints or 30min → detects rhythm anomalies
     * - If pace-pulse ran recently (< 5 min), this hook skips its reminder to avoid
     *   double-reminding the user in a short window.
     *
     * Uses .devpace/.pulse-counter as persistent counter (not version-controlled).
     * Uses .devpace/.pulse-last-run as pace-pulse timestamp (written by advance mode).
     *
     * This is an advisory hook (exit 0), never blocks.
     */
    
    import { existsSync, readFileSync, writeFileSync } from 'node:fs';
    import { basename } from 'node:path';
    import { readStdinJson, getProjectDir, extractFilePath, isCrFile, readCrState } from './lib/utils.mjs';
    
    const input = await readStdinJson();
    const projectDir = getProjectDir();
    const devpaceDir = `${projectDir}/.devpace`;
    const counterFile = `${devpaceDir}/.pulse-counter`;
    const pulseLastRunFile = `${devpaceDir}/.pulse-last-run`;
    
    // Only act if .devpace exists
    if (!existsSync(devpaceDir)) {
      process.exit(0);
    }
    
    // Read and increment counter
    let count = 0;
    try {
      const raw = readFileSync(counterFile, 'utf-8').trim();
      count = parseInt(raw, 10) || 0;
    } catch {
      // Counter file doesn't exist yet — start at 0
    }
    
    count += 1;
    
    // Write updated counter
    try {
      writeFileSync(counterFile, String(count), 'utf-8');
    } catch {
      // Can't write counter — degrade silently
      process.exit(0);
    }
    
    // --- Stuck detection: same CR written 5+ times without state change ---
    // Throttled to every 3rd write to reduce per-write I/O (reads JSON + CR file + writes JSON).
    const filePath = extractFilePath(input);
    const backlogDir = `${devpaceDir}/backlog`;
    if (isCrFile(filePath, backlogDir) && count % 3 === 0) {
      const crWritesPath = `${devpaceDir}/.pulse-cr-writes`;
      let writes = {};
      try { writes = JSON.parse(readFileSync(crWritesPath, 'utf-8')); } catch { /* start fresh */ }
    
      const crName = basename(filePath, '.md');
      const currentState = readCrState(filePath);
    
      if (!writes[crName] || writes[crName].last_state !== currentState) {
        writes[crName] = { count: 1, last_state: currentState };
      } else {
        writes[crName].count++;
      }
    
      // Prune stale entries — keep only CRs still in backlog (max 20 as safety cap)
      const keys = Object.keys(writes);
      if (keys.length > 20) {
        for (const k of keys) {
          if (!existsSync(`${backlogDir}/${k}.md`)) {
            delete writes[k];
          }
        }
      }
    
      try { writeFileSync(crWritesPath, JSON.stringify(writes), 'utf-8'); } catch { /* silent */ }
    
      if (writes[crName].count >= 5) {
        console.log(`devpace:stuck-warning ${crName} 已被写入 ${writes[crName].count} 次但状态仍为 ${currentState},可能在空转。ACTION: 执行 /pace-status 检查是否有阻塞项;若有则解决阻塞后继续;若无则审视当前方案——考虑 /pace-change 调整范围或分拆 CR。`);
        console.log(`devpace:struggle-signal ${crName} 重复写入可能指示环境缺陷(Skill/procedure/Schema 不足)。ACTION: 当前继续完成 CR 任务;CR merged 后执行 /pace-learn 萃取改进建议。`);
      }
    }
    
    // Trigger write-volume reminder every 10 writes
    if (count > 0 && count % 10 === 0) {
      // Check if pace-pulse ran recently (< 5 min) — skip if so to avoid double-remind
      let skipReminder = false;
      try {
        const lastRunStr = readFileSync(pulseLastRunFile, 'utf-8').trim();
        const lastRunMs = parseInt(lastRunStr, 10) || 0;
        if (lastRunMs > 0 && (Date.now() - lastRunMs) < 5 * 60 * 1000) {
          skipReminder = true;
        }
      } catch {
        // No pulse-last-run file — pace-pulse hasn't run, proceed normally
      }
    
      if (!skipReminder) {
        console.log(`devpace:write-volume 已执行 ${count} 次写操作。ACTION: 执行 /pace-status 查看项目进度,确认当前工作节奏正常。`);
      }
    }
    
    process.exit(0);
    
  • hooks/session-end.shRunsGitHub
  • hooks/session-start.shRunsGitHub
  • hooks/session-stop.shRunsGitHub
  • hooks/skill-eval.mjsRunsGitHub
  • hooks/subagent-stop.mjsRunsGitHub
  • hooks/sync-push.mjsRunsGitHub

All 12 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

Ships withdevpace

Give your Claude Code projects a steady development pace — requirements change, rhythm stays. A development harness for Claude Code — rules, schemas, gates, and feedback loops that keep AI-assisted development traceable and measurable.

Get the whole plugin
Stats
73
Stars
6
Forks
Maintained
Maintenance
Python
Language
MIT
License
5mo ago
Last commit
7mo ago
Created

Repo: arch-team/devpace