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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 15 additions & 15 deletions .agentkit/docs/getting-started/CLI_INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Or, if adding AgentKit Forge to an existing project, see the
### 2. Install runtime dependencies

```bash
pnpm -C .agentkit install
pnpm --dir .agentkit install
```

This installs the Node.js dependencies (including `js-yaml`) required by the
Expand Down Expand Up @@ -70,17 +70,17 @@ node .agentkit/engines/node/src/cli.mjs <command> [options]
Several commands have shorthand scripts defined in `.agentkit/package.json`:

```bash
pnpm -C .agentkit agentkit:sync # equivalent to: cli.mjs sync
pnpm -C .agentkit agentkit:init # equivalent to: cli.mjs init
pnpm -C .agentkit agentkit:validate # equivalent to: cli.mjs validate
pnpm -C .agentkit agentkit:discover # equivalent to: cli.mjs discover
pnpm -C .agentkit agentkit:spec-validate # equivalent to: cli.mjs spec-validate
pnpm -C .agentkit agentkit:add # equivalent to: cli.mjs add
pnpm -C .agentkit agentkit:remove # equivalent to: cli.mjs remove
pnpm -C .agentkit agentkit:list # equivalent to: cli.mjs list
pnpm -C .agentkit agentkit:healthcheck # equivalent to: cli.mjs healthcheck
pnpm -C .agentkit agentkit:check # equivalent to: cli.mjs check
pnpm -C .agentkit agentkit:cost # equivalent to: cli.mjs cost
pnpm --dir .agentkit agentkit:sync # equivalent to: cli.mjs sync
pnpm --dir .agentkit agentkit:init # equivalent to: cli.mjs init
pnpm --dir .agentkit agentkit:validate # equivalent to: cli.mjs validate
pnpm --dir .agentkit agentkit:discover # equivalent to: cli.mjs discover
pnpm --dir .agentkit agentkit:spec-validate # equivalent to: cli.mjs spec-validate
pnpm --dir .agentkit agentkit:add # equivalent to: cli.mjs add
pnpm --dir .agentkit agentkit:remove # equivalent to: cli.mjs remove
pnpm --dir .agentkit agentkit:list # equivalent to: cli.mjs list
pnpm --dir .agentkit agentkit:healthcheck # equivalent to: cli.mjs healthcheck
pnpm --dir .agentkit agentkit:check # equivalent to: cli.mjs check
pnpm --dir .agentkit agentkit:cost # equivalent to: cli.mjs cost
```

### Flag syntax
Expand Down Expand Up @@ -180,7 +180,7 @@ Run any command with `--help` to see its specific flags.

```bash
# 1. Install dependencies
pnpm -C .agentkit install
pnpm --dir .agentkit install

# 2. Initialize for your repository
node .agentkit/engines/node/src/cli.mjs init --repoName my-project
Expand Down Expand Up @@ -217,7 +217,7 @@ Add a validation step to your CI pipeline to ensure spec files and generated
outputs stay consistent:

```bash
pnpm -C .agentkit install
pnpm --dir .agentkit install
node .agentkit/engines/node/src/cli.mjs spec-validate
node .agentkit/engines/node/src/cli.mjs validate
node .agentkit/engines/node/src/cli.mjs check --bail
Expand Down Expand Up @@ -246,7 +246,7 @@ Verify you are using one of the valid CLI commands listed above.
Make sure you installed dependencies first:

```bash
pnpm -C .agentkit install
pnpm --dir .agentkit install
```

If the error persists, verify your Node.js version is 22.0.0 or higher:
Expand Down
2 changes: 1 addition & 1 deletion .agentkit/docs/getting-started/QUICK_START.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ git submodule update --init --recursive
Then install the runtime dependencies:

```bash
pnpm -C .agentkit install
pnpm --dir .agentkit install
```

> **Note:** If you do not use pnpm, you can also run `npm install` inside the
Expand Down
2 changes: 1 addition & 1 deletion .agentkit/docs/guides/COMMAND_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,7 @@ Task added to AGENT_BACKLOG.md. Run `/team-backend` to begin work.

### Overall: PASS (1 warning)

Run `pnpm -C .agentkit agentkit:sync` to regenerate outdated files.
Run `pnpm --dir .agentkit agentkit:sync` to regenerate outdated files.
```

---
Expand Down
6 changes: 3 additions & 3 deletions .agentkit/docs/reference/MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ git remote add agentkit-forge https://github.com/<org>/agentkit-forge.git
### 2. Install Dependencies

```bash
pnpm -C .agentkit install
pnpm --dir .agentkit install
```

### 3. Re-sync

```bash
pnpm -C .agentkit agentkit:sync
pnpm --dir .agentkit agentkit:sync
```

### 4. Validate

```bash
pnpm -C .agentkit agentkit:validate
pnpm --dir .agentkit agentkit:validate
```

### 5. Commit
Expand Down
2 changes: 1 addition & 1 deletion .agentkit/docs/reference/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Dependencies have not been installed. The CLI now auto-installs on first run; if
**Fix:** Run the package install from the .agentkit directory:

```bash
pnpm -C .agentkit install
pnpm --dir .agentkit install
```

If `pnpm` is not installed, install it first:
Expand Down
154 changes: 151 additions & 3 deletions .agentkit/engines/node/src/__tests__/budget-guard.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,56 @@ budgetPolicy:
expect(result.daily.maxSessions).toBe(20);
expect(result.daily.maxTotalDurationMinutes).toBe(240);
expect(result.daily.maxTotalCommands).toBe(500);
// Regex fallback matches first occurrence of warnAtPercent (session: 75)
// since it doesn't parse YAML hierarchy — this is expected behavior
expect(result.daily.warnAtPercent).toBe(75);
// Section-aware extraction correctly returns the daily-scoped value
expect(result.daily.warnAtPercent).toBe(85);
});

it('should return correct daily.warnAtPercent when different from session.warnAtPercent', () => {
const yaml = `
budgetPolicy:
enforcement: warn
session:
warnAtPercent: 70
daily:
warnAtPercent: 90
`;
const result = extractBudgetPolicyRegex(yaml);
expect(result.session.warnAtPercent).toBe(70);
expect(result.daily.warnAtPercent).toBe(90);
});

it('should not read enforcement from a sibling top-level section (parser scope regression)', () => {
// Regression: getStr previously searched `^ enforcement:` globally in content.
// If a sibling section (e.g. costTracking) has enforcement before budgetPolicy,
// getStr would return the wrong value. getStr must now scope to the budgetPolicy block.
const yaml = `
costTracking:
enforcement: off
budgetPolicy:
enforcement: enforce
session:
maxCommands: 50
`;
const result = extractBudgetPolicyRegex(yaml);
expect(result.enforcement).toBe('enforce');
});
});

// -------------------------------------------------------------------------
// deepMerge — additional edge cases
// -------------------------------------------------------------------------
describe('deepMerge — additional edge cases', () => {
it('should preserve 0 values (not treat as null/undefined)', () => {
const result = deepMerge({ a: 10, b: 5 }, { a: 0 });
expect(result.a).toBe(0);
expect(result.b).toBe(5);
});

it('should not be affected by __proto__ keys', () => {
// deepMerge skips __proto__ key — no prototype pollution
const overrides = JSON.parse('{"__proto__":{"polluted":true},"maxCommands":5}');
expect(() => deepMerge({}, overrides)).not.toThrow();
expect({}.polluted).toBeUndefined();
});
});

Expand Down Expand Up @@ -266,6 +313,34 @@ budgetPolicy:
expect(result.status).toBe('deny');
expect(result.reasons.some((r) => r.includes('Files modified'))).toBe(true);
});

it('should respect maxCommands: 0 — deny immediately on any command', () => {
const policy = {
...DEFAULT_POLICY,
enforcement: 'enforce',
session: { ...DEFAULT_POLICY.session, maxCommands: 0 },
};
writeActiveSession({
commandsRun: [{ command: 'check', timestamp: new Date().toISOString() }],
});

const result = checkSessionBudget(TEST_AGENTKIT, policy);
expect(result.status).toBe('deny');
});

it('should respect warnAtPercent: 0 — warn at any non-zero usage', () => {
const policy = {
...DEFAULT_POLICY,
enforcement: 'warn',
session: { ...DEFAULT_POLICY.session, maxCommands: 10, warnAtPercent: 0 },
};
writeActiveSession({
commandsRun: [{ command: 'check', timestamp: new Date().toISOString() }],
});

const result = checkSessionBudget(TEST_AGENTKIT, policy);
expect(result.status).toBe('warn');
});
});

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -331,6 +406,79 @@ budgetPolicy:
expect(result.status).toBe('warn');
expect(result.reasons.some((r) => r.includes('Daily sessions'))).toBe(true);
});

it('should sum durations without per-session rounding accumulation', () => {
// 10 sessions of 90s each = 900s = exactly 15 min
// Per-session rounding (Math.round(90000 / 60000) = 2) would give 20 min
const today = new Date().toISOString().split('T')[0];
for (let i = 0; i < 10; i++) {
const sid = `dur-sess-${i}`;
writeFileSync(
resolve(TEST_AGENTKIT, 'logs', 'sessions', `session-${sid}.json`),
JSON.stringify({
sessionId: sid,
startTime: `${today}T10:0${i}:00.000Z`,
status: 'completed',
durationMs: 90_000, // 90 seconds
commandsRun: [],
filesModified: 0,
}),
'utf-8'
);
}

const result = checkDailyBudget(TEST_AGENTKIT);
expect(result.metrics.totalDurationMinutes).toBe(15);
});

it('should exclude yesterday sessions from daily totals', () => {
const today = new Date().toISOString().split('T')[0];
const yesterday = new Date(Date.now() - 86_400_000).toISOString().split('T')[0];
// Use YYYYMMDD-prefixed filenames so the getTodaySessions() filename fast-path
// is exercised: yesterday's file is filtered out by name alone without reading content.
const todayYYYYMMDD = today.replace(/-/g, '');
const yesterdayYYYYMMDD = yesterday.replace(/-/g, '');

writeFileSync(
resolve(TEST_AGENTKIT, 'logs', 'sessions', `session-${yesterdayYYYYMMDD}-main.json`),
JSON.stringify({
sessionId: 'yesterday-sess',
startTime: `${yesterday}T10:00:00.000Z`,
status: 'completed',
durationMs: 60 * 60_000,
commandsRun: [{ command: 'build' }],
filesModified: 5,
}),
'utf-8'
);
writeFileSync(
resolve(TEST_AGENTKIT, 'logs', 'sessions', `session-${todayYYYYMMDD}-main.json`),
JSON.stringify({
sessionId: 'today-sess',
startTime: `${today}T08:00:00.000Z`,
status: 'completed',
durationMs: 10 * 60_000,
commandsRun: [{ command: 'check' }],
filesModified: 1,
}),
'utf-8'
);

const result = checkDailyBudget(TEST_AGENTKIT);
expect(result.metrics.sessionCount).toBe(1);
expect(result.metrics.totalDurationMinutes).toBe(10);
expect(result.metrics.totalCommands).toBe(1);
});

it('should skip malformed session JSON without crashing', () => {
writeFileSync(
resolve(TEST_AGENTKIT, 'logs', 'sessions', 'session-corrupt.json'),
'not valid json{{{',
'utf-8'
);

expect(() => checkDailyBudget(TEST_AGENTKIT)).not.toThrow();
});
});

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('fresh install (no node_modules)', () => {
cwd: projectRoot,
timeout: 120_000,
});
expect(result).toContain('[agentkit:sync]');
expect(result).toContain('[retort:sync]');
expect(result).toContain('Dry-run');
expect(
existsSync(join(projectRoot, '.agentkit', 'node_modules', 'js-yaml', 'package.json'))
Expand Down
Loading
Loading