@haposoft/cafekit 0.15.3 → 0.16.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +2 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +510 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +9 -5
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/package.json +14 -4
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +21 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +3 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +58 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const REVIEW_VERDICTS = Object.freeze(['PASS', 'FAIL', 'BLOCKED']);
|
|
4
|
+
const EXECUTION_TIERS = Object.freeze(['Light', 'Standard', 'Deep']);
|
|
5
|
+
const LANES = Object.freeze(['Direct', 'Standard', 'Critical']);
|
|
6
|
+
const DEEP_TASK_SEQUENCE = Object.freeze(['inspector', 'implementer', 'test-runner', 'code-auditor']);
|
|
7
|
+
const CRITICAL_LANE_SEQUENCE = Object.freeze(['inspector', 'implementer', 'test-runner', 'code-auditor']);
|
|
8
|
+
const LANE_DELEGATION = Object.freeze({
|
|
9
|
+
Direct: Object.freeze([]),
|
|
10
|
+
Standard: Object.freeze(['code-auditor']),
|
|
11
|
+
Critical: CRITICAL_LANE_SEQUENCE,
|
|
12
|
+
});
|
|
13
|
+
const LANE_RISK_KEYS = Object.freeze({
|
|
14
|
+
reversibility: ['reversible', 'reversibility', 'irreversible', 'non-reversible'],
|
|
15
|
+
destructive: ['destructive', 'deletion', 'delete', 'destroy'],
|
|
16
|
+
auth: ['auth', 'authentication', 'authorization'],
|
|
17
|
+
payment: ['payment', 'billing', 'charge'],
|
|
18
|
+
privacy: ['privacy', 'pii', 'personal data'],
|
|
19
|
+
data: ['data', 'dataset', 'records'],
|
|
20
|
+
schema: ['schema'],
|
|
21
|
+
migration: ['migration', 'migrate', 'database migration'],
|
|
22
|
+
publicContract: ['publicContract', 'public_contract', 'public contract', 'api contract', 'breaking change', 'backward compatibility'],
|
|
23
|
+
crossRuntime: ['crossRuntime', 'cross-runtime', 'cross runtime', 'cross_service', 'cross-service', 'runtime coupling', 'worker', 'webhook'],
|
|
24
|
+
ambiguity: ['ambiguous', 'ambiguity', 'unclear', 'unknown requirements', 'underspecified'],
|
|
25
|
+
rollback: ['rollback', 'rollback difficulty', 'hard to rollback', 'cannot rollback', 'no rollback'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const APPROVAL_SCHEMA_VERSION = '2.0';
|
|
29
|
+
const LEGACY_APPROVAL_ERROR = `Legacy approval state detected (field \`approved\`). Migration required: replace \`approved\` with \`agent_validated\` and \`user_approved\` per schema v${APPROVAL_SCHEMA_VERSION} (schema_version: "${APPROVAL_SCHEMA_VERSION}"). See spec-state.json template. Refusing to infer user approval.`;
|
|
30
|
+
|
|
31
|
+
function assertLane(lane) {
|
|
32
|
+
if (!LANES.includes(lane)) {
|
|
33
|
+
throw new TypeError(`Unsupported workflow lane: ${String(lane)}`);
|
|
34
|
+
}
|
|
35
|
+
return lane;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function asTrue(value) {
|
|
39
|
+
return value === true || value === 1 || value === 'true' || value === 'yes';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function signalValue(source, keys) {
|
|
43
|
+
return keys.some((key) => asTrue(source[key]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function textValue(input) {
|
|
47
|
+
const files = Array.isArray(input.files) ? input.files : [];
|
|
48
|
+
return [input.title, input.description, input.summary, input.operation, ...files]
|
|
49
|
+
.filter((value) => typeof value === 'string')
|
|
50
|
+
.join(' ')
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function classifyRiskSignals(input = {}) {
|
|
55
|
+
const nestedSignals = input.signals && typeof input.signals === 'object' ? input.signals : {};
|
|
56
|
+
const declaredSignals = [input.riskSignals, input.risks]
|
|
57
|
+
.flatMap((value) => Array.isArray(value) ? value : [])
|
|
58
|
+
.filter((value) => typeof value === 'string')
|
|
59
|
+
.join(' ')
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
const source = {
|
|
62
|
+
...input,
|
|
63
|
+
...nestedSignals,
|
|
64
|
+
...(input.riskSignals && typeof input.riskSignals === 'object' && !Array.isArray(input.riskSignals) ? input.riskSignals : {}),
|
|
65
|
+
};
|
|
66
|
+
const text = `${textValue(input)} ${declaredSignals}`;
|
|
67
|
+
const hasTerm = (terms) => terms.some((term) => {
|
|
68
|
+
const escaped = term.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+');
|
|
69
|
+
return new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`).test(text);
|
|
70
|
+
});
|
|
71
|
+
const reversibility = String(source.reversibility || '').toLowerCase();
|
|
72
|
+
const rollback = String(source.rollback || source.rollbackDifficulty || '').toLowerCase();
|
|
73
|
+
const signals = {
|
|
74
|
+
reversibility: source.reversible === false
|
|
75
|
+
|| ['irreversible', 'non-reversible', 'not reversible'].includes(reversibility)
|
|
76
|
+
|| hasTerm(['irreversible', 'non-reversible', 'not reversible']),
|
|
77
|
+
destructive: signalValue(source, LANE_RISK_KEYS.destructive) || hasTerm(LANE_RISK_KEYS.destructive),
|
|
78
|
+
auth: signalValue(source, LANE_RISK_KEYS.auth) || hasTerm(LANE_RISK_KEYS.auth),
|
|
79
|
+
payment: signalValue(source, LANE_RISK_KEYS.payment) || hasTerm(LANE_RISK_KEYS.payment),
|
|
80
|
+
privacy: signalValue(source, LANE_RISK_KEYS.privacy) || hasTerm(LANE_RISK_KEYS.privacy),
|
|
81
|
+
data: signalValue(source, LANE_RISK_KEYS.data) || hasTerm(LANE_RISK_KEYS.data) || /\bdata\b/.test(declaredSignals),
|
|
82
|
+
schema: signalValue(source, LANE_RISK_KEYS.schema) || hasTerm(LANE_RISK_KEYS.schema),
|
|
83
|
+
migration: signalValue(source, LANE_RISK_KEYS.migration) || hasTerm(LANE_RISK_KEYS.migration),
|
|
84
|
+
publicContract: signalValue(source, LANE_RISK_KEYS.publicContract) || hasTerm(LANE_RISK_KEYS.publicContract),
|
|
85
|
+
crossRuntime: signalValue(source, LANE_RISK_KEYS.crossRuntime) || hasTerm(LANE_RISK_KEYS.crossRuntime),
|
|
86
|
+
ambiguity: signalValue(source, LANE_RISK_KEYS.ambiguity)
|
|
87
|
+
|| ['high', 'severe'].includes(String(source.ambiguity || '').toLowerCase())
|
|
88
|
+
|| hasTerm(LANE_RISK_KEYS.ambiguity),
|
|
89
|
+
rollback: signalValue(source, LANE_RISK_KEYS.rollback)
|
|
90
|
+
|| ['difficult', 'hard', 'none', 'impossible'].includes(rollback)
|
|
91
|
+
|| hasTerm(LANE_RISK_KEYS.rollback),
|
|
92
|
+
};
|
|
93
|
+
return signals;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function explicitReversible(input = {}) {
|
|
97
|
+
const source = {
|
|
98
|
+
...input,
|
|
99
|
+
...(input.signals && typeof input.signals === 'object' ? input.signals : {}),
|
|
100
|
+
...(input.riskSignals && typeof input.riskSignals === 'object' ? input.riskSignals : {}),
|
|
101
|
+
};
|
|
102
|
+
return source.reversible === true
|
|
103
|
+
|| String(source.reversibility || '').toLowerCase() === 'reversible';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function overrideLane(input = {}) {
|
|
107
|
+
if (typeof input === 'string') return input;
|
|
108
|
+
return input.override ?? input.laneOverride ?? input.requestedLane ?? input.lane ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isUserAuthorizedForDowngrade(input = {}) {
|
|
112
|
+
// Explicit user-originated authorization is required for any downgrade from Critical.
|
|
113
|
+
// We accept several synonyms for the authorization flag to allow different callers.
|
|
114
|
+
return input.userAuthorized === true
|
|
115
|
+
|| input.user_authorized === true
|
|
116
|
+
|| input.explicitUserAuthorization === true
|
|
117
|
+
|| input.downgradeAuthorized === true
|
|
118
|
+
|| input.confirmDowngrade === true
|
|
119
|
+
|| input.authorization === 'user'
|
|
120
|
+
|| input.origin === 'user'
|
|
121
|
+
|| input.requestedBy === 'user'
|
|
122
|
+
|| input.user_approved === true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function classifyLane(input = {}) {
|
|
126
|
+
if (typeof input === 'string') return { lane: assertLane(input), automaticLane: input, overridden: false, warnings: [], risks: [] };
|
|
127
|
+
if (!input || typeof input !== 'object') throw new TypeError('Lane classification input must be an object');
|
|
128
|
+
|
|
129
|
+
const signals = classifyRiskSignals(input);
|
|
130
|
+
const risks = Object.entries(signals).filter(([, active]) => active).map(([name]) => name);
|
|
131
|
+
const taskCount = input.taskCount === undefined ? 1 : input.taskCount;
|
|
132
|
+
if (!Number.isInteger(taskCount) || taskCount < 1) throw new RangeError('taskCount must be a positive integer');
|
|
133
|
+
|
|
134
|
+
const automaticLane = risks.length > 0
|
|
135
|
+
? 'Critical'
|
|
136
|
+
: explicitReversible(input) && input.lowRisk === true && input.isolated === true && taskCount <= 2
|
|
137
|
+
? 'Direct'
|
|
138
|
+
: 'Standard';
|
|
139
|
+
const requested = overrideLane(input);
|
|
140
|
+
if (requested === null || requested === undefined) {
|
|
141
|
+
return {
|
|
142
|
+
lane: automaticLane,
|
|
143
|
+
automaticLane,
|
|
144
|
+
overridden: false,
|
|
145
|
+
warnings: [],
|
|
146
|
+
risks,
|
|
147
|
+
signals,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
assertLane(requested);
|
|
151
|
+
const isDowngrade = LANES.indexOf(requested) < LANES.indexOf(automaticLane);
|
|
152
|
+
if (isDowngrade && !isUserAuthorizedForDowngrade(input)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Downgrade from ${automaticLane} to ${requested} requires explicit user authorization. ` +
|
|
155
|
+
`Automatic ${automaticLane} due to: ${risks.join(', ') || 'default risk policy'}. ` +
|
|
156
|
+
`Provide userAuthorized:true (origin=user) to confirm downgrade and acknowledge reduced review/evidence coverage.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const warnings = [`Explicit lane override selected: ${requested}.`];
|
|
160
|
+
if (requested !== automaticLane) {
|
|
161
|
+
warnings.push(`Override changes automatic ${automaticLane} classification to ${requested}.`);
|
|
162
|
+
if (LANES.indexOf(requested) < LANES.indexOf(automaticLane)) {
|
|
163
|
+
warnings.push(`Downgrading risk lane may reduce review and evidence coverage: ${risks.join(', ') || 'default risk policy'}.`);
|
|
164
|
+
if (isUserAuthorizedForDowngrade(input)) {
|
|
165
|
+
warnings.push(`Downgrade authorized by user - proceeding with ${requested} despite ${automaticLane} signals.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
lane: requested,
|
|
171
|
+
automaticLane,
|
|
172
|
+
overridden: true,
|
|
173
|
+
warnings,
|
|
174
|
+
risks,
|
|
175
|
+
signals,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function lanePolicy(input = {}) {
|
|
180
|
+
const classification = typeof input === 'string'
|
|
181
|
+
? classifyLane(input)
|
|
182
|
+
: input && LANES.includes(input.lane) && Object.hasOwn(input, 'automaticLane')
|
|
183
|
+
? input
|
|
184
|
+
: classifyLane(input);
|
|
185
|
+
const lane = assertLane(classification.lane);
|
|
186
|
+
const delegated = [...LANE_DELEGATION[lane]];
|
|
187
|
+
return {
|
|
188
|
+
lane,
|
|
189
|
+
executionTier: lane === 'Critical' ? 'Deep' : lane === 'Direct' ? 'Light' : 'Standard',
|
|
190
|
+
automaticLane: classification.automaticLane,
|
|
191
|
+
overridden: classification.overridden,
|
|
192
|
+
warnings: [...classification.warnings],
|
|
193
|
+
risks: [...classification.risks],
|
|
194
|
+
delegated,
|
|
195
|
+
requiresSpec: lane !== 'Direct',
|
|
196
|
+
requiresState: lane !== 'Direct',
|
|
197
|
+
qualityGate: lane === 'Direct' ? 'main-session' : lane === 'Standard' ? 'combined-feature-review' : 'strict-evidence',
|
|
198
|
+
shipPoint: lane === 'Direct' ? 'task' : 'feature',
|
|
199
|
+
evidence: lane === 'Critical' ? 'strict' : lane === 'Standard' ? 'bounded' : 'targeted',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isLegacyApprovalObject(obj) {
|
|
204
|
+
return obj && typeof obj === 'object' && 'approved' in obj;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateApprovalSchema(spec) {
|
|
208
|
+
if (!spec || typeof spec !== 'object') return { valid: false, legacy: false, error: 'spec must be an object' };
|
|
209
|
+
const approvals = spec.approvals;
|
|
210
|
+
if (!approvals || typeof approvals !== 'object') return { valid: true, legacy: false };
|
|
211
|
+
// Check for legacy field `approved` in any stage
|
|
212
|
+
for (const [stage, val] of Object.entries(approvals)) {
|
|
213
|
+
if (isLegacyApprovalObject(val)) {
|
|
214
|
+
return {
|
|
215
|
+
valid: false,
|
|
216
|
+
legacy: true,
|
|
217
|
+
error: `Legacy approval state at approvals.${stage}.approved. ${LEGACY_APPROVAL_ERROR}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (val && typeof val === 'object') {
|
|
221
|
+
// also check if missing new fields but has generated/approved only
|
|
222
|
+
if ('generated' in val && !('agent_validated' in val) && !('user_approved' in val) && !('approved' in val)) {
|
|
223
|
+
// This is ambiguous: generated without new fields - treat as not ready but not legacy
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Check schema version
|
|
228
|
+
const version = spec.schema_version || spec.approval_schema_version;
|
|
229
|
+
if (version && version !== APPROVAL_SCHEMA_VERSION) {
|
|
230
|
+
return { valid: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
231
|
+
}
|
|
232
|
+
if (!version && Object.keys(approvals).length > 0) {
|
|
233
|
+
// No version but uses new schema - allow for backward compat but warn that version should be present
|
|
234
|
+
// Fail closed only if legacy detected above; otherwise allow.
|
|
235
|
+
return { valid: true, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
236
|
+
}
|
|
237
|
+
return { valid: true, legacy: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function approvalState(state = {}) {
|
|
241
|
+
// Detect legacy at top level or inside approvals map
|
|
242
|
+
const approvalsMap = state && state.approvals && typeof state.approvals === 'object' ? state.approvals : null;
|
|
243
|
+
if (approvalsMap) {
|
|
244
|
+
for (const val of Object.values(approvalsMap)) {
|
|
245
|
+
if (isLegacyApprovalObject(val)) {
|
|
246
|
+
throw new Error(LEGACY_APPROVAL_ERROR);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const source = approvalsMap ? null : state;
|
|
251
|
+
// If source is approvals map itself, we should not try to interpret it as single stage
|
|
252
|
+
// But approvalState is intended for a single stage object; if passed whole approvals map, treat as error?
|
|
253
|
+
// Keep original behavior: if state has approvals, use that, otherwise state itself.
|
|
254
|
+
// For whole-spec case with legacy inside map, we already threw above.
|
|
255
|
+
// For single-stage object:
|
|
256
|
+
const singleSource = state && state.approvals && typeof state.approvals === 'object' && !approvalsMap ? state.approvals : state;
|
|
257
|
+
// Actually simplify: if state has approvals, we already handled legacy; for single stage, source is state itself.
|
|
258
|
+
// If state was {generated:true, agent_validated:true}, then approvalsMap is null, source = state
|
|
259
|
+
const target = approvalsMap ? null : state;
|
|
260
|
+
if (target && isLegacyApprovalObject(target)) {
|
|
261
|
+
throw new Error(LEGACY_APPROVAL_ERROR);
|
|
262
|
+
}
|
|
263
|
+
// For single stage case, use target
|
|
264
|
+
// For map case, we shouldn't be called with map; but if called with spec object containing approvals map, we return not-ready?
|
|
265
|
+
if (approvalsMap) {
|
|
266
|
+
// This is a spec-level object, not a stage. Return aggregate? For compatibility, treat as not ready.
|
|
267
|
+
// Check if any stage has valid approval? But spec-level ready requires all stages.
|
|
268
|
+
// We'll just return not ready with legacy check already done.
|
|
269
|
+
return { generated: false, agent_validated: false, user_approved: false, ready: false, schema_version: APPROVAL_SCHEMA_VERSION, legacy: false };
|
|
270
|
+
}
|
|
271
|
+
const src = target || state;
|
|
272
|
+
// Final legacy check on src
|
|
273
|
+
if (isLegacyApprovalObject(src)) {
|
|
274
|
+
throw new Error(LEGACY_APPROVAL_ERROR);
|
|
275
|
+
}
|
|
276
|
+
const result = {
|
|
277
|
+
generated: src?.generated === true,
|
|
278
|
+
agent_validated: src?.agent_validated === true,
|
|
279
|
+
user_approved: src?.user_approved === true,
|
|
280
|
+
};
|
|
281
|
+
return { ...result, ready: result.generated && result.agent_validated && result.user_approved, schema_version: APPROVAL_SCHEMA_VERSION };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function assertVerdict(verdict) {
|
|
285
|
+
if (!REVIEW_VERDICTS.includes(verdict)) {
|
|
286
|
+
throw new TypeError(`Unsupported review verdict: ${String(verdict)}`);
|
|
287
|
+
}
|
|
288
|
+
return verdict;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function executionPolicy({ flash = false, parallel = false } = {}) {
|
|
292
|
+
if (flash && parallel) {
|
|
293
|
+
return {
|
|
294
|
+
allowed: false,
|
|
295
|
+
failFast: true,
|
|
296
|
+
mode: 'flash-parallel-conflict',
|
|
297
|
+
reason: '--flash cannot be combined with --parallel; no execution starts',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
allowed: true,
|
|
302
|
+
failFast: false,
|
|
303
|
+
mode: parallel ? 'parallel' : flash ? 'flash' : 'standard',
|
|
304
|
+
reason: null,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 } = {}) {
|
|
309
|
+
if (lane !== null && lane !== undefined) {
|
|
310
|
+
const policy = lanePolicy(lane);
|
|
311
|
+
return {
|
|
312
|
+
lane: policy.lane,
|
|
313
|
+
executionTier: policy.executionTier,
|
|
314
|
+
tier: tier || null,
|
|
315
|
+
mode,
|
|
316
|
+
delegated: policy.delegated,
|
|
317
|
+
qualityGate: policy.qualityGate,
|
|
318
|
+
shipPoint: policy.shipPoint,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (!EXECUTION_TIERS.includes(tier)) {
|
|
322
|
+
throw new TypeError(`Unsupported execution tier: ${String(tier)}`);
|
|
323
|
+
}
|
|
324
|
+
if (!Number.isInteger(taskCount) || taskCount < 1) {
|
|
325
|
+
throw new RangeError('taskCount must be a positive integer');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (tier === 'Light') {
|
|
329
|
+
return { tier, mode, delegated: [], qualityGate: 'main-session', shipPoint: 'task' };
|
|
330
|
+
}
|
|
331
|
+
if (tier === 'Standard') {
|
|
332
|
+
return {
|
|
333
|
+
tier,
|
|
334
|
+
mode,
|
|
335
|
+
delegated: ['code-auditor'],
|
|
336
|
+
qualityGate: 'combined-ship-point',
|
|
337
|
+
shipPoint: mode === 'full-spec' ? 'final-task' : 'requested-task',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
tier,
|
|
342
|
+
mode,
|
|
343
|
+
delegated: Array.from({ length: taskCount }, () => [...DEEP_TASK_SEQUENCE]).flat(),
|
|
344
|
+
qualityGate: 'per-task-stage-a-b',
|
|
345
|
+
shipPoint: 'task',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function consumeReviewVerdict(verdict) {
|
|
350
|
+
assertVerdict(verdict);
|
|
351
|
+
if (verdict === 'PASS') return { action: 'proceed', terminal: false };
|
|
352
|
+
if (verdict === 'FAIL') return { action: 'fix-and-rerun', terminal: false };
|
|
353
|
+
return { action: 'stop', terminal: true, blocker: 'review returned BLOCKED' };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isFlashUnverified(task) {
|
|
357
|
+
return task?.status === 'in_progress' && task.receipt === 'FLASH_UNVERIFIED';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isStaleFlashDone(task) {
|
|
361
|
+
return task?.status === 'done' && task.receipt === 'FLASH_UNVERIFIED';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function promoteFlashTask(task, verdict, proof = 'Verification: PASS') {
|
|
365
|
+
if (verdict !== 'NO_TESTS') assertVerdict(verdict);
|
|
366
|
+
if (!isFlashUnverified(task)) return task;
|
|
367
|
+
|
|
368
|
+
if (verdict !== 'PASS') {
|
|
369
|
+
return {
|
|
370
|
+
...task,
|
|
371
|
+
status: 'in_progress',
|
|
372
|
+
receipt: 'FLASH_UNVERIFIED',
|
|
373
|
+
blocker: verdict === 'NO_TESTS'
|
|
374
|
+
? 'awaiting test proof from /hapo:test <feature>'
|
|
375
|
+
: `verification returned ${verdict}`,
|
|
376
|
+
dependencyBlocked: true,
|
|
377
|
+
unblocks: false,
|
|
378
|
+
readyForSync: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (typeof proof !== 'string' || !/^Verification:\s*PASS\b/.test(proof.trim())) {
|
|
383
|
+
throw new TypeError('PASS promotion requires a concrete Verification: PASS receipt');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
...task,
|
|
388
|
+
status: 'in_progress',
|
|
389
|
+
receipt: proof.trim(),
|
|
390
|
+
blocker: null,
|
|
391
|
+
dependencyBlocked: true,
|
|
392
|
+
unblocks: false,
|
|
393
|
+
readyForSync: true,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function finalizeFlashTask(task, operation) {
|
|
398
|
+
if (operation !== 'sync-finalize') return task;
|
|
399
|
+
if (
|
|
400
|
+
task?.status !== 'in_progress'
|
|
401
|
+
|| !/^Verification:\s*PASS\b/.test(String(task.receipt || '').trim())
|
|
402
|
+
|| task.readyForSync !== true
|
|
403
|
+
) return task;
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
...task,
|
|
407
|
+
status: 'done',
|
|
408
|
+
dependencyBlocked: false,
|
|
409
|
+
unblocks: true,
|
|
410
|
+
readyForSync: false,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function flashState(taskRegistry = {}) {
|
|
415
|
+
return Object.entries(taskRegistry)
|
|
416
|
+
.filter(([, task]) => isFlashUnverified(task))
|
|
417
|
+
.map(([taskPath]) => taskPath);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function validateCanonicalReceipt(body, options = {}) {
|
|
421
|
+
if (typeof body !== 'string') return ['verification_state'];
|
|
422
|
+
const failures = [];
|
|
423
|
+
// Unambiguous verification state: must be Verification: PASS exactly
|
|
424
|
+
if (!/^\s*Verification:\s*PASS\s*$/m.test(body)) {
|
|
425
|
+
failures.push('verification_state');
|
|
426
|
+
}
|
|
427
|
+
// Command must be present
|
|
428
|
+
if (!/^\s*Command(?:\(s\))?\s*:/m.test(body)) {
|
|
429
|
+
failures.push('command');
|
|
430
|
+
}
|
|
431
|
+
// Exit or Result must be present with success
|
|
432
|
+
if (!/^\s*Exit\s*:|exit\s+code\s*[:=]|\bResult\s*:\s*PASS\b/im.test(body)) {
|
|
433
|
+
failures.push('exit_result');
|
|
434
|
+
}
|
|
435
|
+
// Provenance: must carry both endpoints with non-empty values on same line
|
|
436
|
+
// Base: <non-whitespace> plus Head: <non-whitespace>, OR base_sha: <non-whitespace> plus head_sha: <non-whitespace>
|
|
437
|
+
const hasBase = /^\s*Base[ \t]*:[ \t]*\S/im.test(body);
|
|
438
|
+
const hasHead = /^\s*Head[ \t]*:[ \t]*\S/im.test(body);
|
|
439
|
+
const hasBaseSha = /\bbase_sha[ \t]*:[ \t]*\S/im.test(body);
|
|
440
|
+
const hasHeadSha = /\bhead_sha[ \t]*:[ \t]*\S/im.test(body);
|
|
441
|
+
if (!((hasBase && hasHead) || (hasBaseSha && hasHeadSha))) {
|
|
442
|
+
failures.push('provenance');
|
|
443
|
+
}
|
|
444
|
+
// Artifact hash: if body mentions artifact, require sha256
|
|
445
|
+
// For strict mode, require at least one sha256 or artifact reference?
|
|
446
|
+
// Requirement says artifact or evidence hash when applicable. We make it optional but if artifact mentioned, require hash.
|
|
447
|
+
// For done tasks that produce artifacts, hash should be present. We enforce that if body contains Artifact|artifact, then sha256 must be present.
|
|
448
|
+
if (/\bartifact\b/i.test(body) && !/sha256:/i.test(body)) {
|
|
449
|
+
failures.push('artifact_hash');
|
|
450
|
+
}
|
|
451
|
+
// Evidence hash: if no artifact but strict evidence hash required, we could require sha256 generally for done tasks?
|
|
452
|
+
// Make it optional for now; but we can require at least one hash when requireArtifactHash true
|
|
453
|
+
if (options.requireArtifactHash && !/sha256:/i.test(body)) {
|
|
454
|
+
failures.push('artifact_hash');
|
|
455
|
+
}
|
|
456
|
+
return failures;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function parseCliArgs(argv) {
|
|
460
|
+
const options = {
|
|
461
|
+
flash: false,
|
|
462
|
+
parallel: false,
|
|
463
|
+
json: false,
|
|
464
|
+
verdict: null,
|
|
465
|
+
task: null,
|
|
466
|
+
action: null,
|
|
467
|
+
lane: null,
|
|
468
|
+
override: null,
|
|
469
|
+
proof: 'Verification: PASS',
|
|
470
|
+
userAuthorized: false,
|
|
471
|
+
origin: null,
|
|
472
|
+
};
|
|
473
|
+
const args = [...argv];
|
|
474
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
475
|
+
const arg = args[i];
|
|
476
|
+
if (arg === '--flash') options.flash = true;
|
|
477
|
+
else if (arg === '--parallel') options.parallel = true;
|
|
478
|
+
else if (arg === '--json') options.json = true;
|
|
479
|
+
else if (arg === '--user-authorized') options.userAuthorized = true;
|
|
480
|
+
else if (arg === '--origin') options.origin = args[++i];
|
|
481
|
+
else if (arg === '--consume-verdict') options.action = 'consume-verdict';
|
|
482
|
+
else if (arg === '--promote-flash') options.action = 'promote-flash';
|
|
483
|
+
else if (arg === '--sync-finalize') options.action = 'sync-finalize';
|
|
484
|
+
else if (arg === '--classify-lane') options.action = 'classify-lane';
|
|
485
|
+
else if (arg === '--lane-policy') options.action = 'lane-policy';
|
|
486
|
+
else if (arg === '--approval-state') options.action = 'approval-state';
|
|
487
|
+
else if (arg === '--validate-approval') options.action = 'validate-approval';
|
|
488
|
+
else if (arg === '--validate-receipt') options.action = 'validate-receipt';
|
|
489
|
+
else if (arg === '--lane') options.lane = args[++i];
|
|
490
|
+
else if (arg === '--override') options.override = args[++i];
|
|
491
|
+
else if (arg === '--verdict') {
|
|
492
|
+
options.verdict = args[++i];
|
|
493
|
+
if (!options.verdict) throw new Error('--verdict requires a value');
|
|
494
|
+
} else if (arg === '--task-json') {
|
|
495
|
+
const raw = args[++i];
|
|
496
|
+
if (!raw) throw new Error('--task-json requires a JSON value');
|
|
497
|
+
options.task = JSON.parse(raw);
|
|
498
|
+
} else if (arg === '--proof') {
|
|
499
|
+
options.proof = args[++i];
|
|
500
|
+
if (!options.proof) throw new Error('--proof requires a value');
|
|
501
|
+
} else {
|
|
502
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return options;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function cliResult(result, json) {
|
|
509
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
|
|
510
|
+
return result.exitCode;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function runCli(argv = process.argv.slice(2)) {
|
|
514
|
+
try {
|
|
515
|
+
const options = parseCliArgs(argv);
|
|
516
|
+
const policy = executionPolicy(options);
|
|
517
|
+
if (!policy.allowed) {
|
|
518
|
+
return cliResult({
|
|
519
|
+
ok: false,
|
|
520
|
+
contract: 'execution-policy',
|
|
521
|
+
...policy,
|
|
522
|
+
message: 'Unsupported flags: --flash and --parallel are incompatible.\nNo spec state, task receipt, worktree, subagent, or commit was created.',
|
|
523
|
+
exitCode: 2,
|
|
524
|
+
}, options.json);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (options.action === 'classify-lane') {
|
|
528
|
+
const input = options.task || {};
|
|
529
|
+
// Merge CLI lane/override and auth flags into input
|
|
530
|
+
const merged = { ...input };
|
|
531
|
+
if (options.lane) merged.override = options.lane;
|
|
532
|
+
if (options.override) merged.override = options.override;
|
|
533
|
+
if (options.userAuthorized) merged.userAuthorized = true;
|
|
534
|
+
if (options.origin) merged.origin = options.origin;
|
|
535
|
+
const classification = classifyLane(merged);
|
|
536
|
+
return cliResult({ ok: true, classification, policy: lanePolicy(classification), exitCode: 0, message: `Lane: ${classification.lane}` }, options.json);
|
|
537
|
+
}
|
|
538
|
+
if (options.action === 'lane-policy') {
|
|
539
|
+
const input = options.task || options.lane;
|
|
540
|
+
if (!input) throw new Error('--lane-policy requires --lane or --task-json');
|
|
541
|
+
const policy = lanePolicy(input);
|
|
542
|
+
return cliResult({ ok: true, policy, exitCode: 0, message: `Lane: ${policy.lane}` }, options.json);
|
|
543
|
+
}
|
|
544
|
+
if (options.action === 'approval-state') {
|
|
545
|
+
if (!options.task) throw new Error('--approval-state requires --task-json');
|
|
546
|
+
const state = approvalState(options.task);
|
|
547
|
+
return cliResult({ ok: true, state, exitCode: 0, message: `Approval ready: ${state.ready}` }, options.json);
|
|
548
|
+
}
|
|
549
|
+
if (options.action === 'validate-approval') {
|
|
550
|
+
if (!options.task) throw new Error('--validate-approval requires --task-json');
|
|
551
|
+
const result = validateApprovalSchema(options.task);
|
|
552
|
+
return cliResult({ ok: result.valid, ...result, exitCode: result.valid ? 0 : 2, message: result.valid ? 'Approval schema valid' : result.error }, options.json);
|
|
553
|
+
}
|
|
554
|
+
if (options.action === 'validate-receipt') {
|
|
555
|
+
if (!options.task) throw new Error('--validate-receipt requires --task-json');
|
|
556
|
+
const body = options.task.body || options.task.receipt || '';
|
|
557
|
+
const failures = validateCanonicalReceipt(body);
|
|
558
|
+
return cliResult({ ok: failures.length === 0, failures, exitCode: failures.length === 0 ? 0 : 2, message: failures.length === 0 ? 'Receipt valid' : `Receipt missing: ${failures.join(', ')}` }, options.json);
|
|
559
|
+
}
|
|
560
|
+
if (options.action === 'consume-verdict') {
|
|
561
|
+
const result = consumeReviewVerdict(options.verdict);
|
|
562
|
+
return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.action}` }, options.json);
|
|
563
|
+
}
|
|
564
|
+
if (options.action === 'promote-flash') {
|
|
565
|
+
if (!options.task) throw new Error('--promote-flash requires --task-json');
|
|
566
|
+
const result = promoteFlashTask(options.task, options.verdict, options.proof);
|
|
567
|
+
return cliResult({ ok: true, task: result, exitCode: 0, message: 'Flash task promotion evaluated.' }, options.json);
|
|
568
|
+
}
|
|
569
|
+
if (options.action === 'sync-finalize') {
|
|
570
|
+
if (!options.task) throw new Error('--sync-finalize requires --task-json');
|
|
571
|
+
const result = finalizeFlashTask(options.task, 'sync-finalize');
|
|
572
|
+
return cliResult({ ok: true, task: result, exitCode: 0, message: 'Flash task sync-finalize evaluated.' }, options.json);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return cliResult({ ok: true, contract: 'execution-policy', ...policy, exitCode: 0, message: `Execution mode: ${policy.mode}` }, options.json);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
process.stderr.write(`${error.message}\n`);
|
|
578
|
+
return 2;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (require.main === module) process.exitCode = runCli();
|
|
583
|
+
|
|
584
|
+
module.exports = {
|
|
585
|
+
REVIEW_VERDICTS,
|
|
586
|
+
EXECUTION_TIERS,
|
|
587
|
+
LANES,
|
|
588
|
+
APPROVAL_SCHEMA_VERSION,
|
|
589
|
+
LEGACY_APPROVAL_ERROR,
|
|
590
|
+
DEEP_TASK_SEQUENCE,
|
|
591
|
+
CRITICAL_LANE_SEQUENCE,
|
|
592
|
+
executionPolicy,
|
|
593
|
+
classifyLane,
|
|
594
|
+
lanePolicy,
|
|
595
|
+
approvalState,
|
|
596
|
+
validateApprovalSchema,
|
|
597
|
+
isLegacyApprovalObject,
|
|
598
|
+
isUserAuthorizedForDowngrade,
|
|
599
|
+
validateCanonicalReceipt,
|
|
600
|
+
delegationPlan,
|
|
601
|
+
consumeReviewVerdict,
|
|
602
|
+
isFlashUnverified,
|
|
603
|
+
isStaleFlashDone,
|
|
604
|
+
promoteFlashTask,
|
|
605
|
+
finalizeFlashTask,
|
|
606
|
+
flashState,
|
|
607
|
+
parseCliArgs,
|
|
608
|
+
runCli,
|
|
609
|
+
};
|
|
@@ -36,7 +36,7 @@ export GEMINI_API_KEY_2="key2" # auto-rotates on rate limit
|
|
|
36
36
|
|
|
37
37
|
**Verify setup**: `python scripts/check_setup.py`
|
|
38
38
|
**Analyze media**: `python scripts/gemini_batch_process.py --files <file> --task <analyze|transcribe|extract>`
|
|
39
|
-
- TIP: When you're asked to analyze an image, check if `gemini` command is available, then use `echo "<prompt to analyze image>" | gemini -y -m
|
|
39
|
+
- TIP: When you're asked to analyze an image, check if `gemini` command is available, then use `echo "<prompt to analyze image>" | gemini -y -m "${GEMINI_MODEL:-gemma-4-31b-it}"`. If `gemini` command is not available, use `python scripts/gemini_batch_process.py --files <file> --task analyze` command.
|
|
40
40
|
|
|
41
41
|
> **Stdin support**: Pipe files via stdin for Gemini analysis (auto-detects PNG/JPG/PDF/WAV/MP3).
|
|
42
42
|
|
|
@@ -66,7 +66,7 @@ Load for detailed guidance:
|
|
|
66
66
|
|
|
67
67
|
## Limits
|
|
68
68
|
|
|
69
|
-
**Formats**: Audio (WAV/MP3/AAC, 9
|
|
69
|
+
**Formats**: Audio (WAV/MP3/AAC, 9 hours 30 minutes), Images (PNG/JPEG/WEBP, 3.6k), Video (MP4/MOV, 6h), PDF (1k pages)
|
|
70
70
|
**Size**: 20MB inline, 2GB File API
|
|
71
71
|
**Important:**
|
|
72
72
|
- If you are going to generate a transcript of the audio, and the audio length is longer than 15 minutes, the transcript often gets truncated due to output token limits in the Gemini API response. To get the full transcript, you need to split the audio into smaller chunks (max 15 minutes per chunk) and transcribe each segment for a complete transcript.
|
|
@@ -17,8 +17,8 @@ Comprehensive guide for audio analysis and speech generation using Gemini API.
|
|
|
17
17
|
|
|
18
18
|
### Specifications
|
|
19
19
|
|
|
20
|
-
- **Maximum length**: 9
|
|
21
|
-
- **Multiple files**: Unlimited count, combined max 9
|
|
20
|
+
- **Maximum length**: 9 hours 30 minutes per request
|
|
21
|
+
- **Multiple files**: Unlimited count, combined max 9 hours 30 minutes
|
|
22
22
|
- **Token rate**: 32 tokens/second (1 minute = 1,920 tokens)
|
|
23
23
|
- **Processing**: Auto-downsampled to 16 Kbps mono
|
|
24
24
|
- **File size limits**:
|
|
@@ -241,10 +241,10 @@ response = client.models.generate_content(
|
|
|
241
241
|
**Token calculation**:
|
|
242
242
|
- 1 minute audio = 1,920 tokens
|
|
243
243
|
- 1 hour audio = 115,200 tokens
|
|
244
|
-
- 9
|
|
244
|
+
- 9 hours 30 minutes = 1,094,400 tokens
|
|
245
245
|
|
|
246
246
|
**Model selection**:
|
|
247
|
-
- Use `
|
|
247
|
+
- Use `GEMINI_MODEL` for transcription and analysis when set; otherwise use `gemma-4-31b-it`.
|
|
248
248
|
|
|
249
249
|
**Reduce costs**:
|
|
250
250
|
- Process only relevant segments using timestamps
|
|
@@ -357,7 +357,7 @@ response = client.models.generate_content(
|
|
|
357
357
|
- 1 minute = 1,920 tokens
|
|
358
358
|
- 10 minutes = 19,200 tokens
|
|
359
359
|
- 1 hour = 115,200 tokens
|
|
360
|
-
- 9
|
|
360
|
+
- 9 hours 30 minutes = 1,094,400 tokens
|
|
361
361
|
|
|
362
362
|
**Example costs** (Gemini 2.5 Flash at $1/1M):
|
|
363
363
|
- 1 hour audio: 115,200 tokens = $0.12
|
|
@@ -365,7 +365,7 @@ response = client.models.generate_content(
|
|
|
365
365
|
|
|
366
366
|
## Limitations
|
|
367
367
|
|
|
368
|
-
- Maximum 9
|
|
368
|
+
- Maximum 9 hours 30 minutes per request
|
|
369
369
|
- Auto-downsampled to 16 Kbps mono (quality loss)
|
|
370
370
|
- Files expire after 48 hours
|
|
371
371
|
- No real-time streaming support
|
|
@@ -23,7 +23,7 @@ Comprehensive guide for video understanding, temporal analysis, and YouTube proc
|
|
|
23
23
|
## Model Selection
|
|
24
24
|
|
|
25
25
|
### Recommended
|
|
26
|
-
- **gemma-4-31b-it**: 1M-2M context, balanced quality/speed
|
|
26
|
+
- **gemma-4-31b-it**: 1M-2M context, balanced quality/speed; override with `GEMINI_MODEL` when needed.
|
|
27
27
|
|
|
28
28
|
### Context Windows
|
|
29
29
|
- **2M token models**: ~2 hours (default) or ~6 hours (low-res)
|