mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-12 19:23:07 +08:00
Compare commits
6 Commits
cbecf5689d
...
fix/gategu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25ea9a771d | ||
|
|
12353c52c4 | ||
|
|
231c1fdbe8 | ||
|
|
209abd403b | ||
|
|
2486732714 | ||
|
|
63f9bfc33f |
4
.github/workflows/maintenance.yml
vendored
4
.github/workflows/maintenance.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
@@ -27,6 +29,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
1
.github/workflows/reusable-release.yml
vendored
1
.github/workflows/reusable-release.yml
vendored
@@ -42,6 +42,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.tag }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: code-explorer
|
||||
description: Deeply analyzes existing codebase features by tracing execution paths, mapping architecture layers, and documenting dependencies to inform new development.
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
tools: [Read, Grep, Glob]
|
||||
---
|
||||
|
||||
## Prompt Defense Baseline
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: comment-analyzer
|
||||
description: Analyze code comments for accuracy, completeness, maintainability, and comment rot risk.
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
tools: [Read, Grep, Glob]
|
||||
---
|
||||
|
||||
## Prompt Defense Baseline
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: type-design-analyzer
|
||||
description: Analyze type design for encapsulation, invariant expression, usefulness, and enforcement.
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
tools: [Read, Grep, Glob]
|
||||
---
|
||||
|
||||
## Prompt Defense Baseline
|
||||
|
||||
@@ -30,10 +30,15 @@ As of 2026-05-13:
|
||||
Linear project status updates remain the active tracking surfaces until the
|
||||
workspace is upgraded or issue capacity is freed.
|
||||
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
|
||||
- `npm run observability:ready` reports 16/16 readiness on current `main`.
|
||||
- `npm run observability:ready` reports 18/18 readiness on current `main`,
|
||||
including the GitHub/Linear/handoff/roadmap progress-sync contract.
|
||||
- PR #1846 merged as `797f283036904128bb1b348ae62019eb9f08cf39` and made
|
||||
npm registry signature verification a durable workflow-security gate:
|
||||
workflows that run `npm audit` now need `npm audit signatures`.
|
||||
- PR #1848 merged as `cbecf5689d8d1bd5915e7031697a1d56aac538f2` and added
|
||||
`docs/security/supply-chain-incident-response.md`, plus a workflow-security
|
||||
validator rule blocking `pull_request_target` workflows from restoring or
|
||||
saving shared dependency caches.
|
||||
- `docs/architecture/harness-adapter-compliance.md` maps Claude Code, Codex,
|
||||
OpenCode, Cursor, Gemini, Zed-adjacent, dmux, Orca, Superset, Ghast, and
|
||||
terminal-only support to install paths, verification commands, and risk
|
||||
@@ -56,6 +61,8 @@ As of 2026-05-13:
|
||||
release-readiness evidence refresh: 70/70 harness audit, adapter compliance
|
||||
PASS, 16/16 observability readiness, 2376/2376 root Node tests, markdownlint,
|
||||
release-surface and npm publish-surface tests, and 462/462 `ecc2` Rust tests.
|
||||
- After #1848, `node tests/run-all.js` reports 2377/2377 and the current
|
||||
observability gate reports 18/18.
|
||||
- A detached clean worktree at
|
||||
`bfacf37715b39655cbc2c48f12f2a35c67cb0253` verified Claude plugin tag
|
||||
dry-run without `--force`, local marketplace discovery, temp-home local
|
||||
@@ -218,10 +225,10 @@ is not complete unless the evidence column exists and has been freshly verified.
|
||||
|
||||
| Prompt requirement | Required artifact or gate | Current evidence | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| Keep public PRs below 20 | Repo-family PR recheck | 0 open PRs across the tracked public repos on 2026-05-13 after merging #1846 | Complete for this checkpoint |
|
||||
| Keep public PRs below 20 | Repo-family PR recheck | 0 open PRs across the tracked public repos on 2026-05-13 after merging #1848 | Complete for this checkpoint |
|
||||
| Keep public issues below 20 | Repo-family issue recheck | 0 open issues across the tracked public repos on 2026-05-13 | Complete for this checkpoint |
|
||||
| Manage repository discussions | Repo-family discussion recheck | Latest trunk discussion GraphQL sweep returned closed discussions only; satellite repos remain disabled or empty | Complete for this checkpoint |
|
||||
| Manage PR discussions | PR review/comment closure plus merge/close state | #1846 merged after current-head CI; no open PRs remain | Complete for this checkpoint |
|
||||
| Manage PR discussions | PR review/comment closure plus merge/close state | #1848 merged after current-head CI; no open PRs remain | Complete for this checkpoint |
|
||||
| Salvage useful stale work | `docs/stale-pr-salvage-ledger.md` | Ledger records salvaged, superseded, skipped, and manual-review tails; #1815-#1818 added cost tracking, skill scout, frontend design guidance, code-reviewer false-positive guardrails, and the May 12 gap pass | Complete except translation/manual review tail |
|
||||
| ECC 2.0 preview pack ready | Release docs, quickstart, publication readiness, release notes | `docs/releases/2.0.0-rc.1/` and readiness docs are in-tree; May 13 evidence refresh records harness, adapter, observability, Node, lint, release-surface, npm publish-surface, and Rust checks | Needs final clean-checkout release approval |
|
||||
| Hermes specialized skills included safely | Hermes setup/import docs and sanitized skill surface | Hermes setup and import playbook are public; secrets stay local | Needs final release review |
|
||||
@@ -230,20 +237,21 @@ is not complete unless the evidence column exists and has been freshly verified.
|
||||
| Articles, tweets, and announcements | X thread, LinkedIn copy, GitHub release copy, push checklist | Draft launch collateral exists under rc.1 release docs | Needs URL-backed refresh |
|
||||
| AgentShield enterprise iteration | Policy gates, SARIF, packs, provenance, corpus, HTML reports, exception lifecycle audit, baseline drift Action/CLI surfaces, enterprise research roadmap | PRs #53, #55-#64 landed with test evidence; native PDF export deferred in favor of self-contained HTML plus print-to-PDF until explicit enterprise demand appears; `docs/architecture/agentshield-enterprise-research-roadmap.md` selects baseline drift as the first control-plane slice | Baseline-drift Action and CLI write surfaces landed; evidence-pack routing remains |
|
||||
| ECC Tools next-level app | Billing audit, PR checks, deep analyzer, sync backlog, evaluator/RAG corpus | PRs #26-#40 landed with test evidence | Needs capacity-backed Linear rollout |
|
||||
| GitGuardian/Dependabot/CodeRabbit-style checks | Non-blocking taxonomy and deterministic follow-up checks | ECC-Tools risk taxonomy check plus follow-up signals landed, including Skill Quality, Deep Analyzer Evidence, Analyzer Corpus Evidence, RAG/Evaluator Evidence, and PR Review/Salvage Evidence | Partially complete |
|
||||
| GitGuardian/Dependabot/CodeRabbit-style checks | Non-blocking taxonomy, deterministic follow-up checks, and local supply-chain gates | ECC-Tools risk taxonomy check plus follow-up signals landed, including Skill Quality, Deep Analyzer Evidence, Analyzer Corpus Evidence, RAG/Evaluator Evidence, and PR Review/Salvage Evidence; #1846 added npm registry signature gates; #1848 added the supply-chain incident-response playbook and `pull_request_target` cache-poisoning validator guard | Partially complete |
|
||||
| Harness-agnostic learning system | Audit, adapter matrix, observability, traces, promotion loop | Audit/adapters/observability gates plus `docs/architecture/evaluator-rag-prototype.md`, `examples/evaluator-rag-prototype/`, and ECC-Tools PR #40 define read-only stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison scenarios with trace, report, playbook, verifier, and predictive-check artifacts | Local corpus complete; hosted integration remains future |
|
||||
| Linear roadmap is detailed | Linear project status plus repo mirror | Repo mirror exists; issue creation was retried on 2026-05-12 and remains blocked by the workspace free issue limit | Needs recurring status updates after each merge batch |
|
||||
| Flow separation and progress tracking | Flow lanes with owner artifacts and update cadence | This roadmap defines lanes below | Active |
|
||||
| Realtime Linear sync | Project updates while issue limit is blocked; issues later | ECC-Tools #39 implements opt-in Linear API sync for deferred follow-up backlog items | Needs workspace capacity/config rollout |
|
||||
| Observability for self-use | Local readiness gate, traces, status snapshots, HUD/status contract, risk ledger | `npm run observability:ready` reports 16/16 | Complete for local gate |
|
||||
| Flow separation and progress tracking | Flow lanes with owner artifacts and update cadence | This roadmap defines lanes below and `docs/architecture/progress-sync-contract.md` makes GitHub/Linear/handoff/roadmap sync part of the readiness gate | Active |
|
||||
| Realtime Linear sync | Project updates while issue limit is blocked; issues later | ECC-Tools #39 implements opt-in Linear API sync for deferred follow-up backlog items; `docs/architecture/progress-sync-contract.md` defines the local file-backed realtime boundary while issue capacity is blocked | Needs workspace capacity/config rollout |
|
||||
| Observability for self-use | Local readiness gate, traces, status snapshots, HUD/status contract, risk ledger, progress-sync contract | `npm run observability:ready` reports 18/18 | Complete for local gate |
|
||||
| Proper release and notifications | Release tag, npm publish state, plugin state, social posts | Publication readiness gate exists with May 12 dry-run and May 13 readiness evidence | Not complete; approval/live URLs required |
|
||||
|
||||
## Execution Lanes And Tracking Contract
|
||||
|
||||
Until Linear issue capacity is cleared, this document is the durable execution
|
||||
ledger and Linear receives project status updates only. When capacity is
|
||||
available, each lane below should become a small set of Linear issues linked
|
||||
back to the repo evidence and merge commits.
|
||||
ledger and Linear receives project status updates only. The sync contract lives
|
||||
at `docs/architecture/progress-sync-contract.md`. When capacity is available,
|
||||
each lane below should become a small set of Linear issues linked back to the
|
||||
repo evidence and merge commits.
|
||||
|
||||
| Lane | Source of truth | Next tracked artifact | Update cadence |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -253,7 +261,7 @@ back to the repo evidence and merge commits.
|
||||
| Evaluation and RAG | Reference-set validation, harness audit, traces, ECC-Tools corpus | Read-only evaluator/RAG prototype plus stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison fixtures | Hosted retrieval/check-run automation plan |
|
||||
| AgentShield enterprise | AgentShield PR evidence and roadmap notes | Baseline-drift evidence-pack and backlog sync follow-up | Next implementation batch |
|
||||
| ECC Tools app | ECC-Tools PR evidence, billing audit, risk taxonomy, evaluator/RAG corpus | Capacity-backed Linear rollout | Next implementation batch |
|
||||
| Linear progress | Linear project status updates and this mirror | Status update with queue/evidence/missing gates | Every significant merge batch |
|
||||
| Linear progress | Linear project status updates, `docs/architecture/progress-sync-contract.md`, and this mirror | Status update with queue/evidence/missing gates | Every significant merge batch |
|
||||
|
||||
The project status update should always include:
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ operator needs.
|
||||
`tool-usage.jsonl` events that ECC2 can sync.
|
||||
- Risk ledger: `ecc2/src/observability/mod.rs` scores tool calls and stores a
|
||||
paginated ledger for review.
|
||||
- Progress sync: `docs/architecture/progress-sync-contract.md` defines how
|
||||
GitHub, Linear, local handoffs, the repo roadmap, and `scripts/work-items.js`
|
||||
stay aligned during merge batches and release-gate reviews.
|
||||
|
||||
## Reference Pressure
|
||||
|
||||
@@ -64,9 +67,12 @@ later, but only after the local event model is useful enough to trust.
|
||||
operator dashboard.
|
||||
5. Run `node scripts/session-inspect.js --list-adapters` to confirm which
|
||||
session surfaces are available.
|
||||
6. Use ECC2 tool logs for risky operations, conflict analysis, and handoff
|
||||
6. Run `node scripts/work-items.js sync-github --repo <owner/repo>` before
|
||||
relying on local work-item status for a tracked repository.
|
||||
7. Use ECC2 tool logs for risky operations, conflict analysis, and handoff
|
||||
review before increasing autonomy.
|
||||
|
||||
The end-state is practical: before asking ECC to run larger multi-agent loops,
|
||||
the operator can prove the system has live status, durable session traces,
|
||||
baseline scorecards, and a local risk ledger.
|
||||
baseline scorecards, a local risk ledger, and a progress-sync contract that
|
||||
keeps GitHub, Linear, handoffs, and roadmap evidence from drifting apart.
|
||||
|
||||
67
docs/architecture/progress-sync-contract.md
Normal file
67
docs/architecture/progress-sync-contract.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Progress Sync Contract
|
||||
|
||||
ECC 2.0 tracks execution state across GitHub, Linear, local handoffs, and the
|
||||
repo roadmap. This contract defines the minimum evidence required before a
|
||||
status update can claim a lane is current.
|
||||
|
||||
## Sources Of Truth
|
||||
|
||||
| Surface | Role | Current rule |
|
||||
| --- | --- | --- |
|
||||
| GitHub PRs/issues/discussions | Public queue and review state | Recheck live counts before every significant merge batch and before release approval. |
|
||||
| Linear project | Executive roadmap and stakeholder status update | Post project status updates while issue capacity blocks issue creation. Create/reuse issues only when workspace capacity is available. |
|
||||
| Local handoff | Durable operator continuity | Update the active handoff after every merge batch, queue drain, skipped release gate, or blocked external action. |
|
||||
| Repo roadmap | Auditable planning mirror | Keep `docs/ECC-2.0-GA-ROADMAP.md` aligned to merged PR evidence and unresolved gates. |
|
||||
| `scripts/work-items.js` | Local tracker bridge | Sync GitHub PRs/issues into the SQLite work-items store for status snapshots and blocked follow-up. |
|
||||
|
||||
## Flow Lanes
|
||||
|
||||
The repo mirror uses these flow lanes so ECC work does not collapse into one
|
||||
undifferentiated backlog:
|
||||
|
||||
- Queue hygiene and stale-work salvage
|
||||
- Release, naming, plugin publication, and announcements
|
||||
- Harness adapter compliance
|
||||
- Local observability, HUD/status, and session control
|
||||
- Evaluator/RAG and self-improving harness loops
|
||||
- AgentShield enterprise security platform
|
||||
- ECC Tools billing, PR-risk checks, deep analysis, and Linear sync
|
||||
- Legacy artifact audit and translator/manual-review tails
|
||||
|
||||
Each flow lane needs one owner artifact, one current evidence source, and one
|
||||
next action. A lane is not current if any of those three fields are missing.
|
||||
|
||||
## Significant Merge Batch Update
|
||||
|
||||
After a significant merge batch, update Linear and the handoff with:
|
||||
|
||||
1. Current public queue counts for tracked GitHub repos.
|
||||
2. Merged PR numbers, commit IDs, and validation evidence.
|
||||
3. Changed release gates, if any.
|
||||
4. Deferred or skipped work and the explicit reason.
|
||||
5. The next one or two implementation slices.
|
||||
|
||||
When Linear issue capacity is unavailable, use a project status update instead
|
||||
of creating placeholder issues. When issue capacity is available, create or
|
||||
reuse exact-title issues and link them to the repo evidence.
|
||||
|
||||
## Realtime Boundary
|
||||
|
||||
The local realtime path is file-backed by default:
|
||||
|
||||
- `node scripts/work-items.js sync-github --repo <owner/repo>` imports current
|
||||
GitHub PR and issue state into the SQLite work-items store.
|
||||
- `node scripts/status.js --json` and `node scripts/work-items.js list --json`
|
||||
expose local state for a HUD, handoff, or later Linear sync.
|
||||
- Linear remains the external status surface; the repo does not require hosted
|
||||
telemetry to be release-ready.
|
||||
|
||||
Hosted telemetry such as PostHog can be added later, but it must consume the
|
||||
same event model rather than becoming a second source of truth.
|
||||
|
||||
## Release Gate
|
||||
|
||||
Do not publish, tag, announce, submit marketplace packages, or claim plugin
|
||||
availability from this contract alone. Release readiness still requires the
|
||||
publication-readiness evidence documents, fresh queue checks, package checks,
|
||||
plugin checks, and explicit maintainer approval.
|
||||
@@ -2,7 +2,7 @@
|
||||
name: code-explorer
|
||||
description: 通过追踪执行路径、映射架构层和记录依赖关系,深入分析现有代码库功能,为新的开发提供信息。
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
tools: [Read, Grep, Glob]
|
||||
---
|
||||
|
||||
# 代码探索代理
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: comment-analyzer
|
||||
description: 分析代码注释的准确性、完整性、可维护性和注释腐烂风险。
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
tools: [Read, Grep, Glob]
|
||||
---
|
||||
|
||||
# 注释分析代理
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: type-design-analyzer
|
||||
description: 分析封装、不变式表达、实用性和强制性的类型设计。
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
tools: [Read, Grep, Glob]
|
||||
---
|
||||
|
||||
# 类型设计分析代理
|
||||
|
||||
@@ -108,6 +108,18 @@ function findViolations(filePath, source) {
|
||||
}
|
||||
|
||||
if (WRITE_PERMISSION_PATTERN.test(source)) {
|
||||
for (const step of checkoutSteps) {
|
||||
if (!/persist-credentials:\s*['"]?false['"]?\b/m.test(step.text)) {
|
||||
violations.push({
|
||||
filePath,
|
||||
event: 'write-permission checkout',
|
||||
description: 'workflows with write permissions must disable checkout credential persistence',
|
||||
expression: 'actions/checkout without persist-credentials: false',
|
||||
line: step.startLine,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(NPM_CI_PATTERN)) {
|
||||
violations.push({
|
||||
filePath,
|
||||
|
||||
@@ -42,7 +42,374 @@ const EDIT_WRITE_HOOK_ID = 'pre:edit-write:gateguard-fact-force';
|
||||
const BASH_HOOK_ID = 'pre:bash:gateguard-fact-force';
|
||||
const ECC_DISABLE_VALUES = new Set(['0', 'false', 'off', 'disabled', 'disable']);
|
||||
|
||||
const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|git\s+clean\s+-f|drop\s+table|delete\s+from|truncate|git\s+push\s+--force(?!-with-lease)|git\s+commit\s+--amend|dd\s+if=)\b/i;
|
||||
// SQL-keyword + dd patterns stay as a single regex — they are stable
|
||||
// phrases without shell-flag ordering concerns. Quoted strings are
|
||||
// stripped before this regex runs so a commit message mentioning
|
||||
// "drop table" no longer triggers a false positive.
|
||||
const DESTRUCTIVE_SQL_DD = /\b(drop\s+table|delete\s+from|truncate|dd\s+if=)\b/i;
|
||||
|
||||
/**
|
||||
* Strip the contents of single- and double-quoted strings so phrases
|
||||
* mentioned inside a commit message or echoed argument do not trigger
|
||||
* the destructive detector. Command substitutions are scanned separately
|
||||
* before this runs because they execute even inside double quotes.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string}
|
||||
*/
|
||||
function stripQuotedStrings(input) {
|
||||
return input
|
||||
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
|
||||
.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote subshell delimiters to top-level segment separators so the
|
||||
* destructive check applies inside `$(...)` and backtick subshells.
|
||||
* Without this, `echo y | $(rm -rf /tmp)` and ``echo y | `rm -rf /tmp` ``
|
||||
* slip past the segment splitter because the destructive command lives
|
||||
* inside a sub-expression. Run iteratively to handle a layer of nesting.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string}
|
||||
*/
|
||||
function explodeSubshells(input) {
|
||||
let out = input;
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const before = out;
|
||||
out = out.replace(/\$\(([^()`]*)\)/g, ';$1;');
|
||||
out = out.replace(/`([^`]*)`/g, ';$1;');
|
||||
if (out === before) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract executable command-substitution bodies from a shell line. Single
|
||||
* quotes are literal, so substitutions inside them are ignored; double quotes
|
||||
* still permit substitutions, so those bodies are scanned before quoted text
|
||||
* is stripped.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractCommandSubstitutions(input) {
|
||||
const source = String(input || '');
|
||||
const substitutions = [];
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
const prev = source[i - 1];
|
||||
|
||||
if (ch === '\\' && !inSingle) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '`') {
|
||||
let body = '';
|
||||
i += 1;
|
||||
while (i < source.length) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '`') {
|
||||
break;
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '$' && source[i + 1] === '(') {
|
||||
let depth = 1;
|
||||
let body = '';
|
||||
i += 2;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '(') {
|
||||
depth += 1;
|
||||
} else if (inner === ')') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return substitutions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a command line into top-level segments at unquoted shell
|
||||
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
|
||||
* (`$(...)` / backticks). Quoted strings are stripped first so
|
||||
* separators inside quotes are not split on. Per-segment comments
|
||||
* are also stripped.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function splitCommandSegments(input) {
|
||||
const stripped = explodeSubshells(stripQuotedStrings(input));
|
||||
return stripped
|
||||
.split(/[;|&]+/)
|
||||
.map(segment => segment.replace(/(^|\s)#.*/, '$1').trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize a single command segment by whitespace. Quoted strings
|
||||
* are already collapsed to empty quotes by `stripQuotedStrings`, so
|
||||
* naive whitespace splitting is sufficient.
|
||||
*
|
||||
* @param {string} segment
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function tokenize(segment) {
|
||||
return segment.split(/\s+/).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a leading path and trailing `.exe` from a command token so
|
||||
* `/usr/bin/git`, `git.exe`, and `GIT` all normalize to `git`.
|
||||
*
|
||||
* @param {string} token
|
||||
* @returns {string}
|
||||
*/
|
||||
function commandBasename(token) {
|
||||
if (!token) return '';
|
||||
return token.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect `rm` invocations that recursively force-delete files. Handles
|
||||
* combined (`-rf`, `-fr`, `-Rf`) and split (`-r -f`) flag forms.
|
||||
*
|
||||
* @param {string[]} tokens
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDestructiveRm(tokens) {
|
||||
if (tokens.length === 0 || commandBasename(tokens[0]) !== 'rm') return false;
|
||||
let hasR = false;
|
||||
let hasF = false;
|
||||
for (const t of tokens.slice(1)) {
|
||||
if (t === '--recursive') {
|
||||
hasR = true;
|
||||
continue;
|
||||
}
|
||||
if (t === '--force') {
|
||||
hasF = true;
|
||||
continue;
|
||||
}
|
||||
if (!t.startsWith('-') || t.startsWith('--')) continue;
|
||||
const body = t.slice(1);
|
||||
if (/[rR]/.test(body)) hasR = true;
|
||||
if (/f/.test(body)) hasF = true;
|
||||
}
|
||||
return hasR && hasF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the git subcommand within a token list, skipping over git's
|
||||
* global options like `-c key=value`, `-C <path>`, `--git-dir=...`,
|
||||
* `--work-tree=...`, `--namespace=...`, `--super-prefix=...`.
|
||||
*
|
||||
* @param {string[]} tokens
|
||||
* @returns {{ command: string, rest: string[] } | null}
|
||||
*/
|
||||
function findGitSubcommand(tokens) {
|
||||
if (tokens.length === 0 || commandBasename(tokens[0]) !== 'git') return null;
|
||||
const valueConsumingShort = new Set(['-c', '-C']);
|
||||
const valueConsumingLong = new Set(['--git-dir', '--work-tree', '--namespace', '--super-prefix']);
|
||||
let i = 1;
|
||||
while (i < tokens.length) {
|
||||
const t = tokens[i];
|
||||
if (valueConsumingShort.has(t) || valueConsumingLong.has(t)) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (t.startsWith('--git-dir=') || t.startsWith('--work-tree=') || t.startsWith('--namespace=') || t.startsWith('--super-prefix=')) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (t.startsWith('-')) {
|
||||
// Unknown global option — skip without consuming a value.
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
return { command: t.toLowerCase(), rest: tokens.slice(i + 1) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect destructive `git` invocations: `reset --hard`, `checkout --`,
|
||||
* `clean -f...`, `push --force` (but not `--force-with-lease`),
|
||||
* `commit --amend`, `rm -rf`.
|
||||
*
|
||||
* @param {string[]} tokens
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDestructiveGit(tokens) {
|
||||
const sub = findGitSubcommand(tokens);
|
||||
if (!sub) return false;
|
||||
const { command, rest } = sub;
|
||||
|
||||
if (command === 'reset') {
|
||||
return rest.includes('--hard');
|
||||
}
|
||||
|
||||
if (command === 'checkout') {
|
||||
return rest.includes('--');
|
||||
}
|
||||
|
||||
if (command === 'clean') {
|
||||
// `git clean -f`, `-fd`, `-fdx`, `-df`, `--force`
|
||||
return rest.some(t => {
|
||||
if (t === '--force') return true;
|
||||
if (!t.startsWith('-') || t.startsWith('--')) return false;
|
||||
return t.slice(1).includes('f');
|
||||
});
|
||||
}
|
||||
|
||||
if (command === 'push') {
|
||||
// Only `--force-with-lease` qualifies as a safety-checked force.
|
||||
// `--force-if-includes` is a no-op when used WITHOUT
|
||||
// `--force-with-lease` (per git-scm.com/docs/git-push), and when
|
||||
// combined with a bare `--force` the bare force is still in effect.
|
||||
// So `--force --force-if-includes` must be treated as destructive.
|
||||
//
|
||||
// A `+` refspec prefix (e.g. `git push origin +main`,
|
||||
// `+refs/heads/main:refs/heads/main`) also forces a non-fast-forward
|
||||
// update of that ref and is destructive on its own.
|
||||
let withLease = false;
|
||||
let bareForce = false;
|
||||
let plusRefspecForce = false;
|
||||
for (const t of rest) {
|
||||
if (t === '--force-with-lease' || t.startsWith('--force-with-lease=')) {
|
||||
withLease = true;
|
||||
continue;
|
||||
}
|
||||
if (t === '--force' || t.startsWith('--force=')) {
|
||||
bareForce = true;
|
||||
continue;
|
||||
}
|
||||
if (t.startsWith('-') && !t.startsWith('--') && t.slice(1).includes('f')) {
|
||||
bareForce = true;
|
||||
continue;
|
||||
}
|
||||
// Refspec prefix: `+<src>[:<dst>]`. Match tokens like `+main`,
|
||||
// `+refs/heads/main`, `+HEAD:branch`, `+:branch`. Exclude bare
|
||||
// `+` and numeric-only `+123` which are not refspecs.
|
||||
if (t.startsWith('+') && t.length > 1 && /^\+(?:[a-zA-Z_/.:]|HEAD)/.test(t)) {
|
||||
plusRefspecForce = true;
|
||||
}
|
||||
}
|
||||
return bareForce || (plusRefspecForce && !withLease);
|
||||
}
|
||||
|
||||
if (command === 'commit') {
|
||||
return rest.includes('--amend');
|
||||
}
|
||||
|
||||
if (command === 'rm') {
|
||||
// `git rm -r` / `-rf` / `-r -f` — destructive within the index too.
|
||||
let hasR = false;
|
||||
for (const t of rest) {
|
||||
if (!t.startsWith('-') || t.startsWith('--')) continue;
|
||||
if (/[rR]/.test(t.slice(1))) hasR = true;
|
||||
}
|
||||
return hasR;
|
||||
}
|
||||
|
||||
if (command === 'switch') {
|
||||
// `git switch` can discard local working-tree changes in three forms:
|
||||
// --discard-changes explicit discard
|
||||
// --force / -f ignore conflicts and overwrite
|
||||
// -C <branch> force-create (overwrites existing branch)
|
||||
return rest.some(t => {
|
||||
if (t === '--discard-changes' || t === '--force') return true;
|
||||
if (!t.startsWith('-') || t.startsWith('--')) return false;
|
||||
// Short combined form: -f, -fC, -Cf, -C
|
||||
const body = t.slice(1);
|
||||
return /[fC]/.test(body);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether a bash command line contains a destructive action
|
||||
* the fact-forcing gate should challenge. Combines SQL-keyword
|
||||
* detection (regex on quote-stripped input) with per-segment shell
|
||||
* tokenization for shell commands.
|
||||
*
|
||||
* @param {string} command
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDestructiveBash(command) {
|
||||
// The SQL/dd phrases live in command bodies, not as flag-bearing
|
||||
// arguments, so we still match them by regex — but on the input
|
||||
// after quoting AND subshell delimiters are normalized so phrases
|
||||
// inside `$(...)` or backticks are also caught.
|
||||
const raw = String(command || '');
|
||||
const flattened = explodeSubshells(stripQuotedStrings(raw));
|
||||
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
|
||||
|
||||
const segments = [raw, ...extractCommandSubstitutions(raw)].flatMap(splitCommandSegments);
|
||||
for (const segment of segments) {
|
||||
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
|
||||
const tokens = tokenize(segment);
|
||||
if (isDestructiveRm(tokens)) return true;
|
||||
if (isDestructiveGit(tokens)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- State management (per-session, atomic writes, bounded) ---
|
||||
|
||||
@@ -483,7 +850,7 @@ function run(rawInput) {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
if (DESTRUCTIVE_BASH.test(command)) {
|
||||
if (isDestructiveBash(command)) {
|
||||
// Gate destructive commands on first attempt; allow retry after facts presented
|
||||
const key = '__destructive__' + crypto.createHash('sha256').update(command).digest('hex').slice(0, 16);
|
||||
if (!isChecked(key)) {
|
||||
|
||||
@@ -124,6 +124,9 @@ function buildChecks(rootDir) {
|
||||
const sessionManagerRust = readText(rootDir, 'ecc2/src/session/manager.rs');
|
||||
const readinessDoc = readText(rootDir, 'docs/architecture/observability-readiness.md');
|
||||
const hudStatusContract = readText(rootDir, 'docs/architecture/hud-status-session-control.md');
|
||||
const progressSyncContract = readText(rootDir, 'docs/architecture/progress-sync-contract.md');
|
||||
const gaRoadmap = readText(rootDir, 'docs/ECC-2.0-GA-ROADMAP.md');
|
||||
const workItems = readText(rootDir, 'scripts/work-items.js');
|
||||
const hudStatusFixture = safeParseJson(readText(rootDir, 'examples/hud-status-contract.json')) || {};
|
||||
const quickstart = readText(rootDir, 'docs/releases/2.0.0-rc.1/quickstart.md');
|
||||
const releaseNotes = readText(rootDir, 'docs/releases/2.0.0-rc.1/release-notes.md');
|
||||
@@ -238,6 +241,40 @@ function buildChecks(rootDir) {
|
||||
&& releaseNotes.includes('observability-readiness.md'),
|
||||
fix: 'Add the observability readiness doc and link it from rc.1 release docs.'
|
||||
},
|
||||
{
|
||||
id: 'progress-sync-contract',
|
||||
category: 'Tracker Sync',
|
||||
points: 2,
|
||||
path: 'docs/architecture/progress-sync-contract.md',
|
||||
description: 'Linear, GitHub, handoff, and roadmap progress sync has an evidence-backed contract',
|
||||
pass: fileExists(rootDir, 'docs/architecture/progress-sync-contract.md')
|
||||
&& includesAll(progressSyncContract, [
|
||||
'Linear',
|
||||
'GitHub',
|
||||
'handoff',
|
||||
'work-items',
|
||||
'issue capacity',
|
||||
'status update',
|
||||
'queue counts',
|
||||
'release gate',
|
||||
'flow lanes',
|
||||
'evidence'
|
||||
])
|
||||
&& includesAll(gaRoadmap, [
|
||||
'Execution Lanes And Tracking Contract',
|
||||
'docs/architecture/progress-sync-contract.md',
|
||||
'Linear progress',
|
||||
'Every significant merge batch'
|
||||
])
|
||||
&& includesAll(workItems, [
|
||||
'sync-github',
|
||||
'github-pr',
|
||||
'github-issue',
|
||||
'sourceClosedAt',
|
||||
'ecc-work-items-sync-github'
|
||||
]),
|
||||
fix: 'Add the progress sync contract, link it from the GA roadmap, and preserve work-items GitHub sync.'
|
||||
},
|
||||
{
|
||||
id: 'package-exposes-readiness-gate',
|
||||
category: 'Packaging',
|
||||
|
||||
@@ -122,6 +122,21 @@ function run() {
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects checkout credential persistence in workflows with write permissions', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-write-checkout.yml': `name: Unsafe\non:\n workflow_dispatch:\npermissions:\n contents: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: npm ci --ignore-scripts\n`,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, 'Expected validator to fail on credential-persisting checkout');
|
||||
assert.match(result.stderr, /write permissions must disable checkout credential persistence/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows checkout with disabled credential persistence in workflows with write permissions', () => {
|
||||
const result = runValidator({
|
||||
'safe-write-checkout.yml': `name: Safe\non:\n workflow_dispatch:\npermissions:\n contents: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n persist-credentials: false\n - run: npm ci --ignore-scripts\n`,
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects actions/cache in workflows with id-token write', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-oidc-cache.yml': `name: Unsafe\non:\n push:\npermissions:\n contents: read\n id-token: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/cache@v5\n with:\n path: ~/.npm\n key: cache\n`,
|
||||
|
||||
@@ -1143,6 +1143,145 @@ function runTests() {
|
||||
'second subagent edit should pass even on a new file');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Shell-words tokenizer: bypasses the old regex missed ---
|
||||
|
||||
function expectDestructiveDeny(command, label) {
|
||||
clearState();
|
||||
const input = { tool_name: 'Bash', tool_input: { command } };
|
||||
const result = runBashHook(input);
|
||||
assert.strictEqual(result.code, 0, `${label}: exit code should be 0`);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, `${label}: should produce JSON output`);
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny', `${label}: should deny`);
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive'),
|
||||
`${label}: reason should mention "Destructive"`);
|
||||
}
|
||||
|
||||
function expectAllow(command, label) {
|
||||
clearState();
|
||||
writeState({ checked: ['__bash_session__'], last_active: Date.now() });
|
||||
const input = { tool_name: 'Bash', tool_input: { command } };
|
||||
const result = runBashHook(input);
|
||||
assert.strictEqual(result.code, 0, `${label}: exit code should be 0`);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, `${label}: should produce JSON output`);
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny', `${label}: should not deny`);
|
||||
} else {
|
||||
assert.strictEqual(output.tool_name, 'Bash', `${label}: pass-through should preserve input`);
|
||||
}
|
||||
}
|
||||
|
||||
if (test('denies short-form git push -f as destructive', () => {
|
||||
expectDestructiveDeny('git push -f origin main', 'git push -f');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git reset --hard even with intervening -c global option', () => {
|
||||
expectDestructiveDeny('git -c core.foo=bar reset --hard', 'git -c ... reset --hard');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -fr (reverse flag order)', () => {
|
||||
expectDestructiveDeny('rm -fr /tmp/junk', 'rm -fr');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -r -f (split flag form)', () => {
|
||||
expectDestructiveDeny('rm -r -f /tmp/junk', 'rm -r -f');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm --recursive --force (long flag form)', () => {
|
||||
expectDestructiveDeny('rm --recursive --force /tmp/junk', 'rm --recursive --force');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git reset HEAD --hard (with intervening ref)', () => {
|
||||
expectDestructiveDeny('git reset HEAD --hard', 'git reset HEAD --hard');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git clean -fd (combined force+dirs flag)', () => {
|
||||
expectDestructiveDeny('git clean -fd', 'git clean -fd');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies destructive command in second chained segment', () => {
|
||||
expectDestructiveDeny('echo y | rm -rf /tmp/junk', 'echo y | rm -rf');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies destructive command inside command substitution', () => {
|
||||
expectDestructiveDeny('echo $(rm -rf /tmp/junk)', 'rm -rf inside $()');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies destructive command inside backticks', () => {
|
||||
expectDestructiveDeny('echo `git push -f origin main`', 'git push -f inside backticks');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows destructive phrase quoted inside a commit message', () => {
|
||||
expectAllow('git commit -m "fix: rm -rf race in worker"', 'rm -rf in -m');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows SQL phrase quoted inside a commit message', () => {
|
||||
expectAllow('git commit -m "docs: explain when drop table is safe"', 'drop table in -m');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows git push --force-if-includes as a safety-checked variant', () => {
|
||||
expectAllow('git push --force-with-lease --force-if-includes origin main',
|
||||
'git push --force-if-includes');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Review-round-2 findings ---
|
||||
|
||||
if (test('denies git push --force even with --force-if-includes present', () => {
|
||||
expectDestructiveDeny('git push --force --force-if-includes origin main',
|
||||
'git push --force --force-if-includes');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git push when bare --force is mixed with lease flags', () => {
|
||||
expectDestructiveDeny('git push --force-with-lease --force origin main',
|
||||
'git push --force-with-lease --force');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git push with +refspec prefix (bare branch)', () => {
|
||||
expectDestructiveDeny('git push origin +main', 'git push origin +main');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git push with +refspec prefix (full ref)', () => {
|
||||
expectDestructiveDeny('git push origin +refs/heads/main:refs/heads/main',
|
||||
'git push origin +refs/heads/main:refs/heads/main');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git switch --discard-changes', () => {
|
||||
expectDestructiveDeny('git switch --discard-changes feature',
|
||||
'git switch --discard-changes');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git switch --force', () => {
|
||||
expectDestructiveDeny('git switch --force main', 'git switch --force');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git switch -f short form', () => {
|
||||
expectDestructiveDeny('git switch -f main', 'git switch -f');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git switch -C force-create', () => {
|
||||
expectDestructiveDeny('git switch -C feature', 'git switch -C');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('still allows plain git switch', () => {
|
||||
expectAllow('git switch feature', 'git switch feature');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf nested inside a backtick subshell', () => {
|
||||
expectDestructiveDeny('echo y | `rm -rf /tmp/junk`',
|
||||
'backtick subshell');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf nested inside a $(...) subshell', () => {
|
||||
expectDestructiveDeny('echo y | $(rm -rf /tmp/junk)',
|
||||
'dollar-paren subshell');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf inside double-quoted command substitution', () => {
|
||||
expectDestructiveDeny('echo "$(rm -rf /tmp/junk)"',
|
||||
'double-quoted dollar-paren subshell');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
try {
|
||||
if (fs.existsSync(stateDir)) {
|
||||
|
||||
@@ -57,11 +57,22 @@ function seedMinimalRepo(rootDir, overrides = {}) {
|
||||
'scripts/session-inspect.js': '--list-adapters --write inspectSessionTarget',
|
||||
'scripts/lib/session-adapters/registry.js': 'module.exports = {};',
|
||||
'scripts/harness-audit.js': 'Deterministic harness audit --format overall_score',
|
||||
'scripts/work-items.js': 'sync-github github-pr github-issue sourceClosedAt ecc-work-items-sync-github',
|
||||
'scripts/hooks/session-activity-tracker.js': 'tool-usage.jsonl session_id tool_name',
|
||||
'ecc2/src/observability/mod.rs': 'ToolCallEvent RiskAssessment ToolLogger',
|
||||
'ecc2/src/session/store.rs': 'insert_tool_log query_tool_logs',
|
||||
'ecc2/src/session/manager.rs': 'sync_tool_activity_metrics tool-usage.jsonl',
|
||||
'docs/architecture/observability-readiness.md': 'node scripts/observability-readiness.js --format json',
|
||||
'docs/architecture/progress-sync-contract.md': [
|
||||
'Linear GitHub handoff work-items issue capacity status update',
|
||||
'queue counts release gate flow lanes evidence'
|
||||
].join('\n'),
|
||||
'docs/ECC-2.0-GA-ROADMAP.md': [
|
||||
'Execution Lanes And Tracking Contract',
|
||||
'docs/architecture/progress-sync-contract.md',
|
||||
'Linear progress',
|
||||
'Every significant merge batch'
|
||||
].join('\n'),
|
||||
'docs/architecture/hud-status-session-control.md': [
|
||||
'context toolCalls activeAgents todos checks cost risk queueState',
|
||||
'create resume status stop diff pr mergeQueue conflictQueue',
|
||||
@@ -230,6 +241,23 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('missing progress sync contract fails without disturbing core tool checks', () => {
|
||||
const projectRoot = createTempDir('observability-readiness-sync-fail-');
|
||||
|
||||
try {
|
||||
seedMinimalRepo(projectRoot, {
|
||||
'docs/architecture/progress-sync-contract.md': null
|
||||
});
|
||||
const report = buildReport(projectRoot);
|
||||
|
||||
assert.strictEqual(report.ready, false);
|
||||
assert.ok(report.checks.some(check => check.id === 'progress-sync-contract' && !check.pass));
|
||||
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log('\nResults:');
|
||||
console.log(` Passed: ${passed}`);
|
||||
console.log(` Failed: ${failed}`);
|
||||
|
||||
Reference in New Issue
Block a user