ecoportal-api-v2 3.3.3 → 3.3.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/ecoportal/api/v2_version.rb +1 -1
- metadata +1 -22
- data/.ai-assistance/code/double_model_patterns.md +0 -133
- data/.ai-assistance/conventions/code-working-tree-protocol.md +0 -176
- data/.ai-assistance/scripts/token-logger.js +0 -220
- data/.ai-assistance/scripts/token-report.ts +0 -158
- data/.ai-assistance/scripts/token-session-start.js +0 -66
- data/.ai-assistance/skills/eP_AI_Manager/SKILL.md +0 -262
- data/.ai-assistance/token-budget.json +0 -44
- data/.claude/settings.json +0 -107
- data/.gitignore +0 -25
- data/.markdownlint.json +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -101
- data/.ruby-version +0 -1
- data/.yardopts +0 -10
- data/CLAUDE.md +0 -82
- data/Gemfile +0 -6
- data/Rakefile +0 -36
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/ecoportal-api-v2.gemspec +0 -40
- data/scripts/auto-worker-scheduler.sh +0 -307
data/.rubocop.yml
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
AllCops:
|
|
2
|
-
TargetRubyVersion: 3.2
|
|
3
|
-
Exclude:
|
|
4
|
-
- 'config/routes.rb'
|
|
5
|
-
NewCops: enable
|
|
6
|
-
|
|
7
|
-
Metrics/ClassLength:
|
|
8
|
-
Max: 500
|
|
9
|
-
Metrics/ModuleLength:
|
|
10
|
-
Max: 300
|
|
11
|
-
Metrics/MethodLength:
|
|
12
|
-
Max: 50
|
|
13
|
-
Metrics/AbcSize:
|
|
14
|
-
Max: 30
|
|
15
|
-
Metrics/ParameterLists:
|
|
16
|
-
Max: 5
|
|
17
|
-
CountKeywordArgs: false
|
|
18
|
-
Metrics/BlockLength:
|
|
19
|
-
CountAsOne: ['array', 'heredoc', 'method_call']
|
|
20
|
-
Max: 50
|
|
21
|
-
Metrics/CyclomaticComplexity:
|
|
22
|
-
Max: 30
|
|
23
|
-
Metrics/PerceivedComplexity:
|
|
24
|
-
Max: 30
|
|
25
|
-
|
|
26
|
-
Style/AccessorGrouping:
|
|
27
|
-
Enabled: false
|
|
28
|
-
Style/ConditionalAssignment:
|
|
29
|
-
Enabled: false
|
|
30
|
-
Style/BlockDelimiters:
|
|
31
|
-
BracesRequiredMethods: ['log']
|
|
32
|
-
Style/HashSyntax:
|
|
33
|
-
EnforcedShorthandSyntax: either
|
|
34
|
-
EnforcedStyle: no_mixed_keys
|
|
35
|
-
Style/ArgumentsForwarding:
|
|
36
|
-
UseAnonymousForwarding: false
|
|
37
|
-
Style/ClassAndModuleChildren:
|
|
38
|
-
Enabled: false
|
|
39
|
-
Style/FrozenStringLiteralComment:
|
|
40
|
-
Enabled: false
|
|
41
|
-
Style/StringLiterals:
|
|
42
|
-
Enabled: false
|
|
43
|
-
Style/StringLiteralsInInterpolation:
|
|
44
|
-
Enabled: false
|
|
45
|
-
Style/Documentation:
|
|
46
|
-
Enabled: false
|
|
47
|
-
Style/CommentedKeyword:
|
|
48
|
-
Enabled: false
|
|
49
|
-
Style/MultilineBlockChain:
|
|
50
|
-
Enabled: false
|
|
51
|
-
Style/AndOr:
|
|
52
|
-
Enabled: false
|
|
53
|
-
Style/Alias:
|
|
54
|
-
EnforcedStyle: prefer_alias_method
|
|
55
|
-
Style/FetchEnvVar:
|
|
56
|
-
Enabled: false
|
|
57
|
-
Style/RegexpLiteral:
|
|
58
|
-
EnforcedStyle: mixed
|
|
59
|
-
AllowInnerSlashes: true
|
|
60
|
-
|
|
61
|
-
Layout/HashAlignment:
|
|
62
|
-
EnforcedColonStyle: table
|
|
63
|
-
EnforcedHashRocketStyle: table
|
|
64
|
-
Layout/LeadingCommentSpace:
|
|
65
|
-
Enabled: false
|
|
66
|
-
AllowGemfileRubyComment: true
|
|
67
|
-
Layout/ParameterAlignment:
|
|
68
|
-
Enabled: false
|
|
69
|
-
Layout/MultilineMethodDefinitionBraceLayout:
|
|
70
|
-
EnforcedStyle: symmetrical
|
|
71
|
-
Layout/LineLength:
|
|
72
|
-
Enabled: true
|
|
73
|
-
Layout/SpaceInsideHashLiteralBraces:
|
|
74
|
-
Enabled: false
|
|
75
|
-
Layout/SpaceInsideBlockBraces:
|
|
76
|
-
Enabled: false
|
|
77
|
-
Layout/SpaceAroundOperators:
|
|
78
|
-
Enabled: false
|
|
79
|
-
Layout/ExtraSpacing:
|
|
80
|
-
AllowForAlignment: true
|
|
81
|
-
AllowBeforeTrailingComments: true
|
|
82
|
-
Layout/AccessModifierIndentation:
|
|
83
|
-
EnforcedStyle: indent
|
|
84
|
-
Layout/DotPosition:
|
|
85
|
-
EnforcedStyle: trailing
|
|
86
|
-
Layout/MultilineMethodCallIndentation:
|
|
87
|
-
EnforcedStyle: indented
|
|
88
|
-
Layout/FirstHashElementIndentation:
|
|
89
|
-
Enabled: false
|
|
90
|
-
Layout/EmptyLineAfterGuardClause:
|
|
91
|
-
Enabled: false
|
|
92
|
-
|
|
93
|
-
Naming/VariableNumber:
|
|
94
|
-
EnforcedStyle: snake_case
|
|
95
|
-
CheckSymbols: false
|
|
96
|
-
Naming/MethodParameterName:
|
|
97
|
-
AllowedNames: ['x', 'y', 'i', 'j', 'id', 'io']
|
|
98
|
-
Naming/RescuedExceptionsVariableName:
|
|
99
|
-
Enabled: false
|
|
100
|
-
Naming/BlockForwarding:
|
|
101
|
-
Enabled: false
|
data/.ruby-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.2.2
|
data/.yardopts
DELETED
data/CLAUDE.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md — ecoportal-api-v2
|
|
2
|
-
|
|
3
|
-
AI agent instructions for this repository.
|
|
4
|
-
|
|
5
|
-
**Cross-cutting architecture context lives in `ecoportal-api-graphql` — see its `CLAUDE.md` and `.claude/` folder for the full dependency map, project history, and shared skills.**
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Repository Role
|
|
10
|
-
|
|
11
|
-
`ecoportal-api-v2` is the **REST API v2 gem**, extending `ecoportal-api` with a rich content model layer (pages, registers, people, S3 file handling). It provides the `Ecoportal::API::V2` and `Ecoportal::API::Common::Content` namespaces.
|
|
12
|
-
|
|
13
|
-
**Position in chain:**
|
|
14
|
-
```
|
|
15
|
-
ecoportal-api
|
|
16
|
-
↓
|
|
17
|
-
ecoportal-api-v2 ← THIS REPO
|
|
18
|
-
↓
|
|
19
|
-
ecoportal-api-graphql
|
|
20
|
-
↓
|
|
21
|
-
eco-helpers
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
**Remote:** https://gitlab.ecoportal.co.nz/oscar/ecoportal-api-v2.git
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Key Folder Layout
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
lib/ecoportal/api/
|
|
32
|
-
common/
|
|
33
|
-
concerns/ Benchmarkable, Threadable mixins
|
|
34
|
-
content/ Core model infrastructure (see below)
|
|
35
|
-
v2/
|
|
36
|
-
page/ Page model — sections, stages, components, forces, permissions
|
|
37
|
-
page/component/ All field types (text, number, date, selection, files, geo, etc.)
|
|
38
|
-
pages/ Pages collection, stage results, tasks
|
|
39
|
-
registers/ Register models — search results, stage/page results
|
|
40
|
-
people/ V2 people endpoint
|
|
41
|
-
s3/ S3 file upload/download infrastructure
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### `Common::Content` — the model engine
|
|
45
|
-
|
|
46
|
-
The most complex part of this gem. Key classes:
|
|
47
|
-
|
|
48
|
-
- `DoubleModel` — two-layer model (a "live" doc and a "patch" doc) with attribute DSL, diffing, nesting, and change tracking. Almost all V2 models inherit from this.
|
|
49
|
-
- `CollectionModel` — ordered collection with upsert/delete/mutation support.
|
|
50
|
-
- `ClassHelpers` — `class_resolver` macro for injectable class references (pattern shared with `ecoportal-api-graphql`).
|
|
51
|
-
- `DocHelpers`, `ModelHelpers`, `IncluderHelpers` — utility mixins for model construction.
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Namespace
|
|
56
|
-
|
|
57
|
-
- `Ecoportal::API::V2` — v2 REST resources
|
|
58
|
-
- `Ecoportal::API::Common::Content` — shared model infrastructure (consumed downstream)
|
|
59
|
-
- `Ecoportal::API::Common::Concerns` — threading and benchmarking mixins
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Key Concerns
|
|
64
|
-
|
|
65
|
-
- `DoubleModel` is the foundational abstraction — understand it before modifying any V2 model.
|
|
66
|
-
- `Common::Content::ClassHelpers` mirrors the `ClassHelpers` in `ecoportal-api-graphql` — changes to one may need mirroring in the other.
|
|
67
|
-
- S3 upload is async with polling (`s3/files/poll.rb`) — timing-sensitive code.
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## Backwards Compatibility
|
|
72
|
-
|
|
73
|
-
`eco-helpers` and `ecoportal-api-graphql` both depend on this gem. Changes to `Common::Content::*` or `V2::Page::*` interfaces must be checked against usage in both downstream gems before committing.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Running Tests
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
bundle install
|
|
81
|
-
bundle exec rspec
|
|
82
|
-
```
|
data/Gemfile
DELETED
data/Rakefile
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
require "bundler/gem_tasks"
|
|
2
|
-
require "rspec/core/rake_task"
|
|
3
|
-
require 'rubocop/rake_task'
|
|
4
|
-
require "yard"
|
|
5
|
-
require "redcarpet"
|
|
6
|
-
|
|
7
|
-
desc "run the specs"
|
|
8
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
-
|
|
10
|
-
desc "run rspec showing backtrace"
|
|
11
|
-
RSpec::Core::RakeTask.new(:spec_trace) do |task|
|
|
12
|
-
task.rspec_opts = ['--backtrace']
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
desc "run rspec stopping on first fail, and show backtrace"
|
|
16
|
-
RSpec::Core::RakeTask.new(:spec_fast) do |task|
|
|
17
|
-
task.rspec_opts = ['--fail-fast', '--backtrace']
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
desc "run rubocop diaplying cop names"
|
|
21
|
-
RuboCop::RakeTask.new(:rubocop) do |t|
|
|
22
|
-
t.options = ['--display-cop-names']
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# default task name is yard
|
|
26
|
-
desc "Yard: generate all the documentation"
|
|
27
|
-
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
28
|
-
#t.files = ['lib/**/*.rb']
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
desc "default task: runs rubocop and rspec"
|
|
32
|
-
task :default do
|
|
33
|
-
Rake::Task[:rubocop].invoke
|
|
34
|
-
ensure
|
|
35
|
-
Rake::Task[:spec].invoke
|
|
36
|
-
end
|
data/bin/console
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "ecoportal/api-v2"
|
|
5
|
-
|
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
-
|
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
-
# require "pry"
|
|
11
|
-
# Pry.start
|
|
12
|
-
|
|
13
|
-
require "pry"
|
|
14
|
-
Pry.start(__FILE__)
|
data/bin/setup
DELETED
data/ecoportal-api-v2.gemspec
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# rubocop:disable Gemspec/DevelopmentDependencies
|
|
2
|
-
lib = File.expand_path('lib', __dir__)
|
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
|
|
5
|
-
require 'ecoportal/api/v2_version'
|
|
6
|
-
|
|
7
|
-
Gem::Specification.new do |spec|
|
|
8
|
-
spec.name = 'ecoportal-api-v2'
|
|
9
|
-
spec.version = Ecoportal::API::GEM2_VERSION
|
|
10
|
-
spec.authors = ['Oscar Segura']
|
|
11
|
-
spec.email = ['oscar@ecoportal.co.nz']
|
|
12
|
-
|
|
13
|
-
spec.summary = "A collection of helpers for interacting with the ecoPortal MS's V2 API"
|
|
14
|
-
spec.homepage = "https://www.ecoportal.com"
|
|
15
|
-
spec.licenses = %w[MIT]
|
|
16
|
-
|
|
17
|
-
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
18
|
-
|
|
19
|
-
spec.required_ruby_version = '>= 3.2.2'
|
|
20
|
-
|
|
21
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
22
|
-
f.match(%r{^(test|spec|features)/})
|
|
23
|
-
end
|
|
24
|
-
spec.bindir = 'exe'
|
|
25
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
26
|
-
spec.require_paths = ['lib']
|
|
27
|
-
|
|
28
|
-
spec.add_development_dependency 'pry', '~> 0.14'
|
|
29
|
-
spec.add_development_dependency 'rake', '>= 13.0.3', '< 14'
|
|
30
|
-
spec.add_development_dependency 'redcarpet', '>= 3.6.0', '< 4'
|
|
31
|
-
spec.add_development_dependency 'rspec', '>= 3.12.0', '< 4'
|
|
32
|
-
spec.add_development_dependency 'rubocop', '~> 1'
|
|
33
|
-
spec.add_development_dependency 'rubocop-rake', '~> 0'
|
|
34
|
-
spec.add_development_dependency 'yard', '~> 0.9'
|
|
35
|
-
|
|
36
|
-
spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.16'
|
|
37
|
-
spec.add_dependency 'mime-types', '~> 3.5', '>= 3.5.2'
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# rubocop:enable Gemspec/DevelopmentDependencies
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# auto-worker-scheduler.sh
|
|
3
|
-
# Scheduler for auto-todo-worker. Called by the Claude Code Stop hook.
|
|
4
|
-
# Reads local config, checks token budget and conflict guard, then
|
|
5
|
-
# launches the worker as a background process if all gates pass.
|
|
6
|
-
#
|
|
7
|
-
# Log format: TIMESTAMP | ACTION | REASON
|
|
8
|
-
# Log file: .ai-assistance/local/auto-worker-changes.log
|
|
9
|
-
#
|
|
10
|
-
# Usage: bash scripts/auto-worker-scheduler.sh
|
|
11
|
-
# (called automatically by Stop hook — no arguments)
|
|
12
|
-
#
|
|
13
|
-
# Requires: jq (for JSON parsing). Falls back to grep/sed on missing jq.
|
|
14
|
-
|
|
15
|
-
set -euo pipefail
|
|
16
|
-
|
|
17
|
-
# ---------------------------------------------------------------------------
|
|
18
|
-
# Paths (all relative — script must be run from repo root)
|
|
19
|
-
# ---------------------------------------------------------------------------
|
|
20
|
-
TOKEN_BUDGET_JSON=".ai-assistance/token-budget.json"
|
|
21
|
-
LAST_RUN_JSON=".ai-assistance/local/auto-worker-last-run.json"
|
|
22
|
-
SESSION_LOCK=".ai-assistance/local/session.lock"
|
|
23
|
-
CHANGES_LOG=".ai-assistance/local/auto-worker-changes.log"
|
|
24
|
-
|
|
25
|
-
# ---------------------------------------------------------------------------
|
|
26
|
-
# Helpers
|
|
27
|
-
# ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
# ai_log_usage <component> <action> <detail> [session_id]
|
|
30
|
-
# Inline copy — scheduler is self-contained; common.sh is not sourced here.
|
|
31
|
-
# Never fails fatally; all errors suppressed.
|
|
32
|
-
ai_log_usage() {
|
|
33
|
-
local component="${1:-unknown}"
|
|
34
|
-
local action="${2:-run}"
|
|
35
|
-
local detail="${3:-}"
|
|
36
|
-
local session_id="${4:-}"
|
|
37
|
-
local log_dir=".ai-assistance/local/kpi"
|
|
38
|
-
local week
|
|
39
|
-
week=$(date +%Y-W%V 2>/dev/null || echo "unknown")
|
|
40
|
-
local log_file="${log_dir}/usage-${week}.jsonl"
|
|
41
|
-
local ts
|
|
42
|
-
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")
|
|
43
|
-
mkdir -p "$log_dir" 2>/dev/null || true
|
|
44
|
-
printf '{"component":"%s","action":"%s","detail":"%s","ts":"%s","session_id":"%s"}\n' \
|
|
45
|
-
"$component" "$action" "$detail" "$ts" "$session_id" >> "$log_file" 2>/dev/null || true
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
log_action() {
|
|
49
|
-
local action="$1"
|
|
50
|
-
local reason="$2"
|
|
51
|
-
local ts
|
|
52
|
-
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
|
|
53
|
-
mkdir -p "$(dirname "$CHANGES_LOG")"
|
|
54
|
-
printf "%s | %s | %s\n" "$ts" "$action" "$reason" >> "$CHANGES_LOG"
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# Read a JSON field using jq if available, otherwise grep+sed fallback.
|
|
58
|
-
# Usage: json_field <file> <dotted.key> e.g. json_field config.json auto_worker.enabled
|
|
59
|
-
json_field() {
|
|
60
|
-
local file="$1"
|
|
61
|
-
local key="$2"
|
|
62
|
-
if command -v jq >/dev/null 2>&1; then
|
|
63
|
-
jq -r ".${key} // empty" "$file" 2>/dev/null
|
|
64
|
-
else
|
|
65
|
-
# Minimal fallback: grep for the last segment key, extract bare value.
|
|
66
|
-
local leaf
|
|
67
|
-
leaf=$(printf '%s' "$key" | sed 's/.*\.//')
|
|
68
|
-
grep -o "\"${leaf}\"[[:space:]]*:[[:space:]]*[^,}]*" "$file" 2>/dev/null \
|
|
69
|
-
| tail -1 \
|
|
70
|
-
| sed 's/.*:[[:space:]]*//' \
|
|
71
|
-
| tr -d '"' \
|
|
72
|
-
| tr -d ' '
|
|
73
|
-
fi
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
# ---------------------------------------------------------------------------
|
|
77
|
-
# Gate 1: auto_worker.enabled
|
|
78
|
-
# ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
if [ ! -f "$TOKEN_BUDGET_JSON" ]; then
|
|
81
|
-
log_action "SKIP" "token-budget.json not found — auto_worker not configured"
|
|
82
|
-
ai_log_usage "script/auto-worker-scheduler" "skip" "auto_worker not enabled -- token-budget.json not found"
|
|
83
|
-
exit 0
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
enabled=$(json_field "$TOKEN_BUDGET_JSON" "auto_worker.enabled")
|
|
87
|
-
if [ "$enabled" != "true" ]; then
|
|
88
|
-
log_action "SKIP" "auto_worker.enabled is false or absent in token-budget.json"
|
|
89
|
-
ai_log_usage "script/auto-worker-scheduler" "skip" "auto_worker not enabled"
|
|
90
|
-
exit 0
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
# ---------------------------------------------------------------------------
|
|
94
|
-
# Gate 2: Conflict guard — check for active Claude session via session.lock
|
|
95
|
-
# ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
if [ -f "$SESSION_LOCK" ]; then
|
|
98
|
-
lock_content=""
|
|
99
|
-
if [ -r "$SESSION_LOCK" ]; then
|
|
100
|
-
lock_content=$(cat "$SESSION_LOCK" 2>/dev/null || true)
|
|
101
|
-
fi
|
|
102
|
-
log_action "SKIP" "Active Claude session detected (session.lock present: ${lock_content})"
|
|
103
|
-
ai_log_usage "script/auto-worker-scheduler" "skip" "session.lock present"
|
|
104
|
-
exit 0
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
# ---------------------------------------------------------------------------
|
|
108
|
-
# Gate 3: Token budget threshold check
|
|
109
|
-
# ---------------------------------------------------------------------------
|
|
110
|
-
# Find the current ISO week KPI file. Format: weekly-YYYY-WNN.json
|
|
111
|
-
# We compute ISO week ourselves for portability.
|
|
112
|
-
|
|
113
|
-
iso_week() {
|
|
114
|
-
# Returns YYYY-WNN string for today (ISO 8601 week numbering)
|
|
115
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
116
|
-
python3 -c "
|
|
117
|
-
import datetime
|
|
118
|
-
today = datetime.date.today()
|
|
119
|
-
iso = today.isocalendar()
|
|
120
|
-
print('%04d-W%02d' % (iso[0], iso[1]))
|
|
121
|
-
"
|
|
122
|
-
elif command -v python >/dev/null 2>&1; then
|
|
123
|
-
python -c "
|
|
124
|
-
import datetime
|
|
125
|
-
today = datetime.date.today()
|
|
126
|
-
iso = today.isocalendar()
|
|
127
|
-
print('%04d-W%02d' % (iso[0], iso[1]))
|
|
128
|
-
"
|
|
129
|
-
else
|
|
130
|
-
# Fallback: use date command Week-of-year (not strictly ISO but close enough)
|
|
131
|
-
date +"%Y-W%V" 2>/dev/null || date +"%Y-W%U"
|
|
132
|
-
fi
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
KPI_DIR=".ai-assistance/local/kpi"
|
|
136
|
-
WEEK_KEY=$(iso_week)
|
|
137
|
-
KPI_FILE="${KPI_DIR}/weekly-${WEEK_KEY}.json"
|
|
138
|
-
|
|
139
|
-
# Read quota and usage from KPI file (if present)
|
|
140
|
-
weekly_tokens_used=0
|
|
141
|
-
weekly_quota=0
|
|
142
|
-
window_start_ts="" # ISO timestamp of first session start in current 5-hour window
|
|
143
|
-
|
|
144
|
-
if [ -f "$KPI_FILE" ] && command -v jq >/dev/null 2>&1; then
|
|
145
|
-
# Sum tokens_used across all sessions
|
|
146
|
-
weekly_tokens_used=$(jq '[.sessions[]?.tokens_used // 0] | add // 0' "$KPI_FILE" 2>/dev/null || echo "0")
|
|
147
|
-
weekly_quota=$(jq -r '.quota // 0' "$KPI_FILE" 2>/dev/null || echo "0")
|
|
148
|
-
|
|
149
|
-
# Find most recent session start to compute reset window
|
|
150
|
-
window_start_ts=$(jq -r '
|
|
151
|
-
[.sessions[]?.session_start // empty] | sort | last // empty
|
|
152
|
-
' "$KPI_FILE" 2>/dev/null || true)
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
# Read quota from token-budget.json as fallback
|
|
156
|
-
if [ "$weekly_quota" = "0" ] || [ -z "$weekly_quota" ]; then
|
|
157
|
-
quota_from_config=$(json_field "$TOKEN_BUDGET_JSON" "weekly_quota.total_tokens")
|
|
158
|
-
if [ "$quota_from_config" != "null" ] && [ -n "$quota_from_config" ]; then
|
|
159
|
-
weekly_quota="$quota_from_config"
|
|
160
|
-
fi
|
|
161
|
-
fi
|
|
162
|
-
|
|
163
|
-
# If no quota is set (null), skip the threshold gate entirely
|
|
164
|
-
if [ "$weekly_quota" = "0" ] || [ "$weekly_quota" = "null" ] || [ -z "$weekly_quota" ]; then
|
|
165
|
-
log_action "OK" "No weekly quota set — skipping token threshold gate"
|
|
166
|
-
else
|
|
167
|
-
# Calculate usage percentage
|
|
168
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
169
|
-
usage_pct=$(python3 -c "
|
|
170
|
-
used = float('${weekly_tokens_used}' or '0')
|
|
171
|
-
quota = float('${weekly_quota}')
|
|
172
|
-
print('%.1f' % (used / quota * 100) if quota > 0 else '0')
|
|
173
|
-
")
|
|
174
|
-
else
|
|
175
|
-
usage_pct=0
|
|
176
|
-
fi
|
|
177
|
-
|
|
178
|
-
# Determine time remaining in current 5-hour reset window
|
|
179
|
-
# 5-hour window = 18000 seconds
|
|
180
|
-
window_seconds_elapsed=0
|
|
181
|
-
if [ -n "$window_start_ts" ] && command -v python3 >/dev/null 2>&1; then
|
|
182
|
-
window_seconds_elapsed=$(python3 -c "
|
|
183
|
-
import datetime, sys
|
|
184
|
-
try:
|
|
185
|
-
ts = '${window_start_ts}'.replace('Z', '+00:00')
|
|
186
|
-
start = datetime.datetime.fromisoformat(ts)
|
|
187
|
-
now = datetime.datetime.now(datetime.timezone.utc)
|
|
188
|
-
elapsed = (now - start).total_seconds()
|
|
189
|
-
print(int(max(0, elapsed)))
|
|
190
|
-
except Exception:
|
|
191
|
-
print(0)
|
|
192
|
-
")
|
|
193
|
-
fi
|
|
194
|
-
|
|
195
|
-
WINDOW_DURATION=18000 # 5 hours in seconds
|
|
196
|
-
window_remaining=$(( WINDOW_DURATION - window_seconds_elapsed ))
|
|
197
|
-
if [ "$window_remaining" -lt 0 ]; then
|
|
198
|
-
window_remaining=0
|
|
199
|
-
fi
|
|
200
|
-
window_remaining_min=$(( window_remaining / 60 ))
|
|
201
|
-
|
|
202
|
-
# Determine threshold based on time remaining in window (ADR-007 table)
|
|
203
|
-
# >120 min -> 65%; 60-120 -> 75%; 45-60 -> 80%; 30-45 -> 90%; 15-30 -> 95%; <15 -> skip
|
|
204
|
-
if [ "$window_remaining_min" -lt 15 ]; then
|
|
205
|
-
log_action "SKIP" "Less than 15 min remaining in token reset window — will not launch"
|
|
206
|
-
ai_log_usage "script/auto-worker-scheduler" "skip" "token budget -- less than 15 min remaining in reset window"
|
|
207
|
-
exit 0
|
|
208
|
-
elif [ "$window_remaining_min" -lt 30 ]; then
|
|
209
|
-
threshold=95
|
|
210
|
-
elif [ "$window_remaining_min" -lt 45 ]; then
|
|
211
|
-
threshold=90
|
|
212
|
-
elif [ "$window_remaining_min" -lt 60 ]; then
|
|
213
|
-
threshold=80
|
|
214
|
-
elif [ "$window_remaining_min" -lt 120 ]; then
|
|
215
|
-
threshold=75
|
|
216
|
-
else
|
|
217
|
-
threshold=65
|
|
218
|
-
fi
|
|
219
|
-
|
|
220
|
-
# Compare usage_pct against threshold (integer comparison via python3)
|
|
221
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
222
|
-
over_threshold=$(python3 -c "
|
|
223
|
-
pct = float('${usage_pct}' or '0')
|
|
224
|
-
thr = float('${threshold}')
|
|
225
|
-
print('true' if pct >= thr else 'false')
|
|
226
|
-
")
|
|
227
|
-
else
|
|
228
|
-
over_threshold="false"
|
|
229
|
-
fi
|
|
230
|
-
|
|
231
|
-
if [ "$over_threshold" = "true" ]; then
|
|
232
|
-
log_action "SKIP" "Token usage ${usage_pct}% >= threshold ${threshold}% (${window_remaining_min} min remaining in window)"
|
|
233
|
-
ai_log_usage "script/auto-worker-scheduler" "skip" "token budget at ${usage_pct}% (threshold ${threshold}%)"
|
|
234
|
-
exit 0
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
log_action "OK" "Token gate passed: ${usage_pct}% used, threshold ${threshold}% (${window_remaining_min} min remaining)"
|
|
238
|
-
fi
|
|
239
|
-
|
|
240
|
-
# ---------------------------------------------------------------------------
|
|
241
|
-
# Gate 4: Determine cadence (informational — actual cron cadence managed by
|
|
242
|
-
# CronCreate; this script just logs the recommendation)
|
|
243
|
-
# ---------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
recommended_cadence="2hr"
|
|
246
|
-
if [ -f "$LAST_RUN_JSON" ] && command -v jq >/dev/null 2>&1; then
|
|
247
|
-
completed_count=$(jq '.completed | length' "$LAST_RUN_JSON" 2>/dev/null || echo "0")
|
|
248
|
-
if [ "$completed_count" -gt "0" ]; then
|
|
249
|
-
recommended_cadence="30min"
|
|
250
|
-
fi
|
|
251
|
-
fi
|
|
252
|
-
|
|
253
|
-
# ---------------------------------------------------------------------------
|
|
254
|
-
# Gate 5: dangerously_skip_permissions opt-in
|
|
255
|
-
# ---------------------------------------------------------------------------
|
|
256
|
-
|
|
257
|
-
skip_perms=$(json_field "$TOKEN_BUDGET_JSON" "auto_worker.dangerously_skip_permissions")
|
|
258
|
-
AUTO_WORKER_PROMPT="Run the auto-worker for this repo.
|
|
259
|
-
|
|
260
|
-
Instructions:
|
|
261
|
-
1. Read TODO.md and collect all items tagged \`[auto]\` or \`[auto:last]\`.
|
|
262
|
-
2. If no \`[auto]\` items exist, exit cleanly with a brief log entry.
|
|
263
|
-
3. Plan execution order as a dependency graph — do NOT follow TODO list order.
|
|
264
|
-
- Items without dependencies on each other may run in parallel (branch out when the work is self-contained enough to isolate).
|
|
265
|
-
- \`[auto:last]\` items run only after all \`[auto]\` items complete or are skipped.
|
|
266
|
-
4. For each item:
|
|
267
|
-
- Execute the task.
|
|
268
|
-
- Mark it \`[x]\` in TODO.md on completion.
|
|
269
|
-
- Add any discovered follow-up tasks as new TODO items; tag \`[auto]\` only if they meet the eligibility criteria in standards/tooling/auto-worker-eligibility.md.
|
|
270
|
-
5. Do NOT touch items that are not tagged \`[auto]\` or \`[auto:last]\`.
|
|
271
|
-
6. At the end, write a one-paragraph summary of what was done, skipped, and added to TODO."
|
|
272
|
-
|
|
273
|
-
CLAUDE_FLAGS="--print \"${AUTO_WORKER_PROMPT}\""
|
|
274
|
-
if [ "$skip_perms" = "true" ]; then
|
|
275
|
-
CLAUDE_FLAGS="--dangerously-skip-permissions --print \"${AUTO_WORKER_PROMPT}\""
|
|
276
|
-
fi
|
|
277
|
-
|
|
278
|
-
# ---------------------------------------------------------------------------
|
|
279
|
-
# Launch: run auto-todo-worker as a background process
|
|
280
|
-
# ---------------------------------------------------------------------------
|
|
281
|
-
|
|
282
|
-
log_action "LAUNCH" "Launching auto-todo-worker (cadence recommendation: ${recommended_cadence})"
|
|
283
|
-
ai_log_usage "script/auto-worker-scheduler" "launch" "launching auto-todo-worker"
|
|
284
|
-
|
|
285
|
-
if [[ "${OSTYPE:-}" == "msys" ]] || [[ "${OSTYPE:-}" == "cygwin" ]] || \
|
|
286
|
-
[[ "${OS:-}" == "Windows_NT" ]]; then
|
|
287
|
-
# Windows via Git Bash / WSL
|
|
288
|
-
# Use PowerShell Start-Process to launch detached from the current terminal
|
|
289
|
-
powershell -Command "Start-Process -FilePath 'claude' \
|
|
290
|
-
-ArgumentList '${CLAUDE_FLAGS}' \
|
|
291
|
-
-WorkingDirectory '${PWD}' \
|
|
292
|
-
-NoNewWindow" &
|
|
293
|
-
launch_pid=$!
|
|
294
|
-
else
|
|
295
|
-
# Unix / macOS / WSL2
|
|
296
|
-
if [ "$skip_perms" = "true" ]; then
|
|
297
|
-
nohup claude --dangerously-skip-permissions --print "run auto worker" \
|
|
298
|
-
> ".ai-assistance/local/auto-worker-stdout.log" 2>&1 &
|
|
299
|
-
else
|
|
300
|
-
nohup claude --print "run auto worker" \
|
|
301
|
-
> ".ai-assistance/local/auto-worker-stdout.log" 2>&1 &
|
|
302
|
-
fi
|
|
303
|
-
launch_pid=$!
|
|
304
|
-
fi
|
|
305
|
-
|
|
306
|
-
log_action "LAUNCHED" "Background PID ${launch_pid:-unknown} — flags: ${CLAUDE_FLAGS}"
|
|
307
|
-
exit 0
|