Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ── Tracker (GitHub) ────────────────────────────────────────────────
# Fine-grained PAT。权限:repo(contents:read, issues:write, pull_requests:write)
# 与 project(read/write)。真实值绝不提交。
GITHUB_TOKEN=
GITHUB_PROJECT_OWNER=firstsun-dev
GITHUB_PROJECT_NUMBER=6
GITHUB_PROJECT_STATUS_FIELD=Status

# ── Agent (Claude Code) ─────────────────────────────────────────────
ANTHROPIC_API_KEY=
FIRSTSUN_AGENT_PLAN=claude-code
FIRSTSUN_AGENT_IMPLEMENT=claude-code
FIRSTSUN_AGENT_PLAN_MODEL=
FIRSTSUN_AGENT_TIMEOUT_MS=900000

# ── Runtime ─────────────────────────────────────────────────────────
FIRSTSUN_POLL_INTERVAL_MS=60000
FIRSTSUN_MAX_CONCURRENT_ISSUES=2
FIRSTSUN_MAX_ATTEMPTS=3
FIRSTSUN_STATE_DIR=./.firstsun-bot
FIRSTSUN_WORKSPACE_DIR=./.firstsun-bot/workspaces
FIRSTSUN_LOG_LEVEL=info
# true 时:照常执行所有读操作,记录每个本应发生的写操作,但一个都不执行
FIRSTSUN_DRY_RUN=false
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
node_modules/
dist/
coverage/
*.tsbuildinfo

.env
.env.*
!.env.example

# 本地缓存 + agent 工作区(可重建,可能含仓库 clone)
.firstsun-bot/

*.log
.DS_Store
.vitest/
1 change: 1 addition & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm exec commitlint --edit "$1"
9 changes: 9 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
staged=$(git diff --cached --name-only --diff-filter=ACM | grep -v '^\.env\.example$')
if [ -n "$staged" ]; then
if echo "$staged" | xargs -r git diff --cached -- | grep -E '(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|sk-ant-[A-Za-z0-9-]{20,}|gho_[A-Za-z0-9]{36})' > /dev/null; then
echo "pre-commit: staged content looks like it contains a secret token. Aborting commit." >&2
exit 1
fi
fi

pnpm exec lint-staged
4 changes: 4 additions & 0 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"*.{ts,js,mjs}": ["eslint --max-warnings=0", "prettier --check"],
"*.{json,md,yml,yaml}": ["prettier --check"]
}
5 changes: 5 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 依赖必须显式声明才能 import —— 与目录封闭约束同向
hoist=false
strict-peer-dependencies=true
# corepack 已固定版本,禁止意外的 pnpm 版本漂移
package-manager-strict=true
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
12 changes: 12 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules/
dist/
coverage/
pnpm-lock.yaml

# Vendored/harness content managed outside this feature's scope
.agents/
archive/
CLAUDE.md
session-handoff.md
docs/
progress.md
6 changes: 6 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ Before ending a session:
```

Required checks:
- `npm run lint`
- `npm run build`
- `npm test`
- `pnpm run lint`
- `pnpm run build`
- `pnpm test`

## Escalation

Expand Down
6 changes: 4 additions & 2 deletions archive/2026-07.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ commit hash. Debugging narrative and design discussion belong in the commit
message, not here. Start a new `archive/YYYY-MM.md` file when the month rolls
over — don't let a single archive file grow without bound either.

- feat-XXX: [One-line summary] (commit abc1234)
- feat-000: Requirements, Spec & Test Plan written (commit 6bb3550)
- feat-001: Project Setup — pnpm/TS/eslint/vitest/husky tooling scaffolded, `./init.sh` green (commit 8769af7)

## Decisions

<!-- Settled decisions moved out of progress.md once no longer active context. One line: decision + why. -->
- **[Decision]**: [why it was made]
- **pnpm over npm**: `hoist=false` makes undeclared transitive imports fail at resolution — a second layer under the provider-isolation constraint.
- **TypeScript pinned to ~6.0.3, not latest 7.0.2**: no typescript-eslint release supports TS 7 yet, and AC-01~03 depend entirely on eslint zoning. Revisit when typescript-eslint ships TS 7 support (SPEC §2.2).
19 changes: 19 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'refactor', 'test', 'chore', 'build', 'ci', 'perf', 'revert'],
],
'scope-enum': [
2,
'always',
['tracker', 'agent', 'orchestrator', 'config', 'state', 'util', 'docs', 'harness', 'deps'],
],
'scope-empty': [1, 'never'],
'subject-case': [2, 'never', ['upper-case', 'pascal-case', 'start-case']],
'header-max-length': [2, 'always', 100],
'body-max-line-length': [0],
},
};
180 changes: 180 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// @ts-check
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

const banGithubSdkPackages = {
group: ['@octokit', '@octokit/*', 'octokit', '@actions/github'],
message:
'GitHub SDKs may only be imported inside src/providers/tracker/github/. Add the capability to TrackerProvider instead.',
};

const banGithubDeepImport = {
group: ['**/providers/tracker/github/**'],
message:
'Deep-import into providers/tracker/github/ is forbidden. Import the factory from its index.ts instead.',
};

const banClaudeSpawn = {
group: ['node:child_process', 'child_process', 'execa', '@anthropic-ai/*'],
message:
'Process-spawning / Claude SDKs may only be used inside src/providers/agent/claude-code/. Add the capability to AgentProvider instead.',
};

const banClaudeDeepImport = {
group: ['**/providers/agent/claude-code/**'],
message:
'Deep-import into providers/agent/claude-code/ is forbidden. Import the factory from its index.ts instead.',
};

const banAnyProviderDeepImport = {
group: ['**/providers/*/*/**'],
message:
'src/orchestrator/** may only depend on provider *types*, never a concrete provider implementation.',
};

const banFsAndSpawn = {
group: ['node:child_process', 'child_process', 'node:fs', 'fs'],
message:
'src/orchestrator/** must stay side-effect free; no process spawning or direct filesystem access.',
};

const httpZoneBans = {
group: [
'**/providers/*/*/**',
'node:child_process',
'child_process',
'undici',
'node-fetch',
'@octokit',
'@octokit/*',
'octokit',
'@anthropic-ai/*',
],
message:
'src/http/** is read-only: no provider implementations, no process spawning, no HTTP client libs.',
};

const compositionAllowedGithub = {
group: ['**/providers/tracker/github/**/*', '!**/providers/tracker/github/index'],
message: banGithubDeepImport.message,
};

const compositionAllowedClaude = {
group: ['**/providers/agent/claude-code/**/*', '!**/providers/agent/claude-code/index'],
message: banClaudeDeepImport.message,
};

// Every directory below is a distinct "zone": exactly one no-restricted-imports
// block may apply per file, since flat config replaces (not merges) a rule
// setting when multiple matching configs set the same key.
const zoneDirs = [
'src/providers/tracker/github/**',
'src/providers/agent/claude-code/**',
'src/orchestrator/**',
'src/http/**',
'src/composition/**',
];

export default tseslint.config(
{
ignores: ['dist/**', 'coverage/**', 'node_modules/**', '.firstsun-bot/**'],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['src/**/*.ts', 'test/**/*.ts'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// Default zone: everything under src/ that isn't one of the special dirs
// below is banned from both the GitHub SDK and the Claude/spawn surface.
{
files: ['src/**/*.ts'],
ignores: zoneDirs,
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
banGithubSdkPackages,
banGithubDeepImport,
banClaudeSpawn,
banClaudeDeepImport,
],
},
],
},
},
// GitHub tracker provider: may use the GitHub SDK, still may not touch Claude.
{
files: ['src/providers/tracker/github/**/*.ts'],
rules: {
'no-restricted-imports': ['error', { patterns: [banClaudeSpawn, banClaudeDeepImport] }],
},
},
// Claude Code agent provider: may spawn `claude`, still may not touch GitHub.
{
files: ['src/providers/agent/claude-code/**/*.ts'],
rules: {
'no-restricted-imports': ['error', { patterns: [banGithubSdkPackages, banGithubDeepImport] }],
},
},
// Orchestrator: no concrete provider, no fs/spawn, no process.env.
{
files: ['src/orchestrator/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [banAnyProviderDeepImport, banFsAndSpawn, banGithubSdkPackages, banClaudeSpawn],
},
],
'no-restricted-syntax': [
'error',
{
selector: "MemberExpression[object.object.name='process'][object.property.name='env']",
message:
'src/orchestrator/** must not read process.env directly; config is always injected.',
},
{
selector: "MemberExpression[object.name='process'][property.name='env']",
message:
'src/orchestrator/** must not read process.env directly; config is always injected.',
},
],
},
},
// Read-only status UI (feat-013, reserved now).
{
files: ['src/http/**/*.ts'],
rules: {
'no-restricted-imports': ['error', { patterns: [httpZoneBans] }],
'no-restricted-globals': [
'error',
{ name: 'fetch', message: 'src/http/** must not make outbound requests.' },
],
},
},
// Composition root: the only place allowed to import a concrete provider
// factory (its index.ts), never a deep path, never the raw SDKs.
{
files: ['src/composition/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
banGithubSdkPackages,
banClaudeSpawn,
compositionAllowedGithub,
compositionAllowedClaude,
],
},
],
},
},
);
6 changes: 3 additions & 3 deletions feature_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"description": "Write docs/REQUIREMENTS.md (FR/NFR/AC + open questions), docs/SPEC.md (provider interfaces, polling/idempotency design, tooling + pinned versions, husky/commitlint, file tree, implementation order), docs/TEST-PLAN.md (test layers, fakes, case list, requirement traceability)",
"dependencies": [],
"status": "done",
"evidence": "Uncommitted - docs/{REQUIREMENTS,SPEC,TEST-PLAN}.md written; cross-doc traceability checked, no code yet"
"evidence": "Commit 6bb3550 - docs/{REQUIREMENTS,SPEC,TEST-PLAN}.md written; cross-doc traceability checked, no code yet"
},
{
"id": "feat-001",
"name": "Project Setup",
"description": "Initialize package.json (pnpm, ESM, Node>=24), tsconfig, eslint flat config with the 3 boundary zones, vitest, prettier, husky+lint-staged+commitlint, .npmrc; switch init.sh and CLAUDE.md verification commands to pnpm; confirm clean checkout runs ./init.sh. See docs/SPEC.md sections 2.1-2.3.2 and 11 step 1",
"dependencies": ["feat-000"],
"status": "not-started",
"evidence": ""
"status": "done",
"evidence": "Commit 8769af7 - ./init.sh green; TEST-PLAN §5.1 negative checks 1-9,11 verified manually"
},
{
"id": "feat-002",
Expand Down
18 changes: 12 additions & 6 deletions init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ set -e

echo "=== Harness Initialization ==="

echo "=== npm run lint ==="
npm run lint
echo "=== corepack enable ==="
corepack enable

echo "=== npm run build ==="
npm run build
echo "=== pnpm install --frozen-lockfile ==="
pnpm install --frozen-lockfile

echo "=== npm test ==="
npm test
echo "=== pnpm run lint ==="
pnpm run lint

echo "=== pnpm run build ==="
pnpm run build

echo "=== pnpm test ==="
pnpm test

echo "=== Verification Complete ==="
echo ""
Expand Down
Loading