Skip to content
Merged
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 AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Instrucoes do projeto Quiz

## Operacao eficiente e segura

- Para operacoes Git rotineiras, use o auxiliar global `codex-safe-git`.
- Push automatizado so e permitido pelo auxiliar para branches temporarias
deste repositorio; nunca faca push direto para `main` ou `develop`.
- Nao use reset, clean, amend, rebase, force-push ou exclusao de branches sem
autorizacao explicita e verificacao do alvo.
- Agrupe leituras e verificacoes independentes e execute
`npm run verify:local` antes de abrir um PR.
- Preserve alteracoes existentes do usuario; nao as inclua em commits sem
autorizacao explicita.
- Use mensagens de commit curtas em portugues ASCII; nao use comentarios em
ingles nem anexe historico automatico.

## Qualidade e publicacao

- Conteudo T3 deve respeitar manifesto curricular, cobertura e revisoes
pedagogica e linguistica, alem da aprovacao humana aplicavel.
- Automacao, parecer de agente ou passagem nos checks nao autorizam merge,
release ou deploy por si so.
- Mudancas para `main` exigem PR, checks obrigatorios e autorizacao explicita
de release; mantenha o fluxo de reconciliacao documentado.
25 changes: 25 additions & 0 deletions config/github-repository-rulesets/required-pull-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "Require pull request for main and develop",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/main", "refs/heads/develop"],
"exclude": []
}
},
"rules": [
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": true,
"allowed_merge_methods": ["merge", "squash"]
}
}
],
"bypass_actors": []
}
7 changes: 4 additions & 3 deletions config/governance-guidelines.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@
},
{
"id": "branching-and-delivery",
"version": "1.2.0",
"version": "1.3.0",
"status": "active",
"type": "policy",
"owner": "architecture",
"sourcePath": "docs/github/branching-and-delivery-strategy.md",
"sha256": "e8e2a1412b75ed91af5442c817f639aac802df42715edc43bb2af83d40d73f8b",
"sha256": "10d29b7361f105001b08eee0a3a5f3310a0ccef3fedc4507fdff959798d51ca1",
"appliesTo": [
"pull-requests",
"releases",
Expand All @@ -74,7 +74,8 @@
"branch sync validation",
"merge authorization",
"merge message manifest",
"post-merge message audit"
"post-merge message audit",
"restricted local git helper"
]
},
{
Expand Down
18 changes: 18 additions & 0 deletions docs/github/branching-and-delivery-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,30 @@ linguisticos e humanos em ambos os modos.
- nao fazer push direto em `main`
- nao fazer push direto em `develop`
- toda evolucao deve passar por Pull Request
- a ruleset ativa exige Pull Request em `main` e `develop`, inclusive no modo
de mantenedor unico; ela exige zero aprovadores independentes enquanto
vigorar a excecao registrada em #310, sem remover checks ou protecoes atuais
- exigir aprovacao independente quando houver ao menos dois mantenedores
elegiveis; durante a fase de mantenedor unico, aplicar a excecao rastreada em
#310
- usar GitHub Actions como gate minimo de governanca nos PRs
- registrar o modo operacional vigente em toda promocao para `main`

## Operacao local com menos aprovacoes

Para operacoes Git rotineiras, use o auxiliar global `codex-safe-git`, que
aceita somente status, diff de leitura, stage por caminhos explicitos, commit
de caminhos explicitos, troca/criacao de branches e push normal de branches
temporarias deste repositorio. Ele recusa opcoes arbitrarias, amend, reset,
force-push, branches permanentes e destinos diferentes de `dleomil/quiz`.

O auxiliar reduz prompts do Codex para esses comandos delimitados, mas nao
altera o sandbox do sistema operacional nem as protecoes do GitHub. Nao use
regras globais que liberem todo o comando `git`; preserve a aprovacao humana
para operacoes destrutivas, merges, releases, deploys e decisoes editoriais.
Antes de abrir um PR, rode `npm run verify:local`, que agrupa a suite de testes,
lint, formatacao e verificacao do diff.

## Motivacao arquitetural

Este modelo e propositalmente simples. Criar muitas branches permanentes neste momento aumentaria custo de coordenacao sem gerar ganho proporcional. Para a maturidade atual do projeto, `main` e `develop` sao suficientes.
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "npm run validate:agents && npm run test:agent-records && npm run test:record-ledger && npm run test:editorial-runner && npm run test:branch-policy && npm run validate:guidelines && npm run test:guidelines && npm run validate:mcp-governance && npm run test:mcp-governance && npm run validate:content && npm run test:content && npm run test:frontend-security && npm run test:pedagogical-regression && npm run test:ui:content-session && npm run test:ui:t3-pilot-draft && npm run test:ui:dark-mode && npm run test:ui:critical-rules && npm run test:ui:trimester-history-selector && npm run test:ui:question-count-selector && npm run test:ui:geography-language-audit && npm run test:ui:english-language-audit && npm run test:ui:portuguese-language-audit && npm run test:ui:panel-base",
"test": "npm run validate:agents && npm run test:agent-records && npm run test:record-ledger && npm run test:editorial-runner && npm run test:branch-policy && npm run test:codex-safe-git && npm run test:github-pr-ruleset && npm run validate:guidelines && npm run test:guidelines && npm run validate:mcp-governance && npm run test:mcp-governance && npm run validate:content && npm run test:content && npm run test:frontend-security && npm run test:pedagogical-regression && npm run test:ui:content-session && npm run test:ui:t3-pilot-draft && npm run test:ui:dark-mode && npm run test:ui:critical-rules && npm run test:ui:trimester-history-selector && npm run test:ui:question-count-selector && npm run test:ui:geography-language-audit && npm run test:ui:english-language-audit && npm run test:ui:portuguese-language-audit && npm run test:ui:panel-base",
"verify:local": "npm test && npm run lint && npm run format:check && git diff --check",
"test:codex-safe-git": "node tests/governance/codex-safe-git.spec.cjs",
"test:github-pr-ruleset": "node tests/governance/github-pr-ruleset.spec.cjs",
"validate:agents": "node scripts/validate-codex-agents.cjs && node tests/agents/agent-capability-model.spec.cjs && node tests/agents/codex-agent-validator.spec.cjs",
"validate:agent-records": "node scripts/agent-execution-records.cjs",
"test:agent-records": "node tests/agents/agent-execution-records.spec.cjs",
Expand Down
222 changes: 222 additions & 0 deletions scripts/codex-safe-git.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#!/usr/bin/env node
'use strict';

const { spawnSync } = require('node:child_process');
const path = require('node:path');

const ALLOWED_QUIZ_BRANCH_PREFIXES = [
'feature/',
'fix/',
'chore/',
'docs/',
'refactor/',
];

function fail(message) {
const error = new Error(message);
error.code = 'SAFE_GIT_REJECTED';
throw error;
}

function validatePaths(paths) {
if (!paths.length) fail('Informe ao menos um caminho depois de --.');
paths.forEach((filePath) => {
if (
!filePath ||
filePath.startsWith('-') ||
filePath.startsWith(':(') ||
path.isAbsolute(filePath) ||
filePath.split(/[\\/]/).includes('..')
) {
fail(`Caminho nao permitido: ${filePath}`);
}
});
}

function parsePathList(args) {
const separatorIndex = args.indexOf('--');
if (separatorIndex < 0 || separatorIndex !== 0) {
fail('Use -- antes dos caminhos.');
}
const paths = args.slice(1);
validatePaths(paths);
return paths;
}

function validateBranchName(branch) {
if (
typeof branch !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(branch) ||
branch.includes('..') ||
branch.includes('//') ||
branch.endsWith('/') ||
branch.endsWith('.') ||
branch.endsWith('.lock')
) {
fail(`Nome de branch nao permitido: ${branch}`);
}
}

function normalizeQuizRemote(remoteUrl) {
if (typeof remoteUrl !== 'string') return null;
const normalized = remoteUrl
.trim()
.replace(/\.git$/i, '')
.replace(/\/$/, '')
.toLowerCase();
const accepted = [
'https://github.com/dleomil/quiz',
'ssh://git@github.com/dleomil/quiz',
'git@github.com:dleomil/quiz',
];
return accepted.includes(normalized) ? normalized : null;
}

function authorizeQuizPush({ remoteUrl, branch }) {
if (!normalizeQuizRemote(remoteUrl)) {
fail('Push automatico permitido somente para dleomil/quiz.');
}
validateBranchName(branch);
if (
!ALLOWED_QUIZ_BRANCH_PREFIXES.some((prefix) => branch.startsWith(prefix))
) {
fail(
'Push automatico permitido somente para branches temporarias de trabalho.',
);
}
return ['push', 'origin', `HEAD:refs/heads/${branch}`];
}

function resolveCommand(args) {
const [operation, ...rest] = args;
if (!operation) fail('Informe uma operacao segura do Git.');

if (operation === 'status' && rest.length === 0) {
return { gitArgs: ['status', '--short', '--branch'] };
}
if (operation === 'diff') {
if (rest.length === 0) {
return {
gitArgs: ['--no-pager', 'diff', '--no-ext-diff', '--no-textconv'],
};
}
if (rest.length === 1 && rest[0] === '--cached') {
return {
gitArgs: [
'--no-pager',
'diff',
'--no-ext-diff',
'--no-textconv',
'--cached',
],
};
}
if (rest.length === 1 && rest[0] === '--check') {
return {
gitArgs: [
'--no-pager',
'diff',
'--no-ext-diff',
'--no-textconv',
'--check',
],
};
}
if (rest.length === 1 && rest[0] === '--stat') {
return {
gitArgs: [
'--no-pager',
'diff',
'--no-ext-diff',
'--no-textconv',
'--stat',
],
};
}
fail('diff aceita somente --cached, --check ou --stat.');
}
if (operation === 'add') {
return { gitArgs: ['add', '--', ...parsePathList(rest)] };
}
if (operation === 'commit') {
if (rest[0] !== '--message' || !rest[1] || rest[2] !== '--') {
fail(
'Use commit --message <texto> -- <caminhos>; amend nao e permitido.',
);
}
const message = rest[1];
const paths = parsePathList(rest.slice(2));
return {
gitArgs: ['commit', '--only', '--message', message, '--', ...paths],
};
}
if (operation === 'switch') {
if (rest.length === 2 && rest[0] === '--create') {
validateBranchName(rest[1]);
return { gitArgs: ['switch', '--create', rest[1]] };
}
if (rest.length === 1) {
validateBranchName(rest[0]);
return { gitArgs: ['switch', rest[0]] };
}
fail('Use switch <branch> ou switch --create <branch>.');
}
if (operation === 'push-quiz' && rest.length === 0) {
return { operation };
}
fail(`Operacao Git nao permitida: ${operation}`);
}

function captureGit(gitArgs, cwd) {
const result = spawnSync('git', gitArgs, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
});
if (result.error) throw result.error;
if (result.status !== 0) {
fail((result.stderr || 'Falha ao consultar o repositorio Git.').trim());
}
return result.stdout.trim();
}

function execute(args, options = {}) {
const cwd = options.cwd || process.cwd();
const command = resolveCommand(args);
let gitArgs = command.gitArgs;

if (command.operation === 'push-quiz') {
const remoteUrl = captureGit(['remote', 'get-url', 'origin'], cwd);
const branch = captureGit(['branch', '--show-current'], cwd);
gitArgs = authorizeQuizPush({ remoteUrl, branch });
}

const result = spawnSync('git', gitArgs, {
cwd,
stdio: 'inherit',
shell: false,
});
if (result.error) throw result.error;
return result.status === null ? 1 : result.status;
}

if (require.main === module) {
try {
process.exitCode = execute(process.argv.slice(2));
} catch (error) {
process.stderr.write(`codex-safe-git: ${error.message}\n`);
process.exitCode = 2;
}
}

module.exports = {
ALLOWED_QUIZ_BRANCH_PREFIXES,
authorizeQuizPush,
execute,
normalizeQuizRemote,
parsePathList,
resolveCommand,
validateBranchName,
validatePaths,
};
Loading
Loading