feat(session): maintain canonical task state
This commit is contained in:
1 parent
6d4ab9ddac
commit
f7ab0160e8
8 files changed
+338
-15
No files matched your search
@@ -1,8 +1,31 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param([string]$Goal,[string[]]$ChangedFiles=@(),[string[]]$Verified=@(),[string[]]$Pending=@(),[string[]]$ImportantFindings=@(),[switch]$Pointer)
|
param(
|
||||||
|
[string]$Workspace = (Get-Location).Path,
|
||||||
|
[AllowEmptyString()][string]$Goal,
|
||||||
|
[AllowEmptyCollection()][string[]]$Decisions,
|
||||||
|
[AllowEmptyCollection()][string[]]$ChangedFiles,
|
||||||
|
[AllowEmptyCollection()][string[]]$Verified,
|
||||||
|
[AllowEmptyCollection()][string[]]$Pending,
|
||||||
|
[AllowEmptyCollection()][string[]]$Risks,
|
||||||
|
[AllowEmptyCollection()][string[]]$ImportantFindings,
|
||||||
|
[switch]$Pointer,
|
||||||
|
[switch]$Reset
|
||||||
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'state.json'
|
$helper = Join-Path (Split-Path $PSScriptRoot -Parent) 'shared/hooks/scripts/session-state.py'
|
||||||
if ($Pointer) { if (Test-Path $path) { Write-Output 'Task state is available in .ai-session/state.json; read it only if needed.' }; exit 0 }
|
$values = @{ workspace = $Workspace }
|
||||||
function Limit([string[]]$items) { @($items | Where-Object { $_ } | Select-Object -First 12 | ForEach-Object { if ($_.Length -gt 180) { $_.Substring(0,180) } else { $_ } }) }
|
$fields = @{ Goal = 'goal'; Decisions = 'decisions'; ChangedFiles = 'changed'; Verified = 'verified'; Pending = 'open'; Risks = 'risks' }
|
||||||
New-Item $dir -ItemType Directory -Force | Out-Null
|
foreach ($parameter in $fields.Keys) {
|
||||||
[ordered]@{ goal=if($Goal){$Goal.Substring(0,[Math]::Min(240,$Goal.Length))}else{''}; changedFiles=Limit $ChangedFiles; verified=Limit $Verified; pending=Limit $Pending; importantFindings=Limit $ImportantFindings; updatedAt=(Get-Date).ToUniversalTime().ToString('o') } | ConvertTo-Json -Depth 4 | Set-Content $path -Encoding UTF8
|
if ($PSBoundParameters.ContainsKey($parameter)) { $values[$fields[$parameter]] = $PSBoundParameters[$parameter] }
|
||||||
|
}
|
||||||
|
if ($PSBoundParameters.ContainsKey('ImportantFindings') -and -not $PSBoundParameters.ContainsKey('Decisions')) {
|
||||||
|
$values.decisions = $ImportantFindings
|
||||||
|
}
|
||||||
|
$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
||||||
|
$arguments = @($helper, '--update-json')
|
||||||
|
if ($Pointer) { $arguments += '--pointer' }
|
||||||
|
if ($Reset) { $arguments += '--reset' }
|
||||||
|
$payload = $values | ConvertTo-Json -Depth 4 -Compress
|
||||||
|
$payload = [regex]::Replace($payload, '[^\x00-\x7F]', { param($match) '\u{0:x4}' -f [int][char]$match.Value })
|
||||||
|
$payload | & python @arguments
|
||||||
|
exit $LASTEXITCODE
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/state.json"
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
if [ "${1:-}" = --pointer ]; then [ -f "$path" ] && printf 'Task state is available in .ai-session/state.json; read it only if needed.\n'; exit 0; fi
|
arguments=()
|
||||||
goal=${AI_TASK_GOAL:-}; goal=${goal:0:240}; goal_json=$(printf '%s' "$goal" | sed 's/[\\]/\\\\/g; s/"/\\"/g'); mkdir -p "$dir"
|
if [ "${AI_TASK_GOAL+x}" = x ]; then arguments+=(--goal "$AI_TASK_GOAL"); fi
|
||||||
printf '{"goal":"%s","changedFiles":[],"verified":[],"pending":[],"importantFindings":[],"updatedAt":"%s"}\n' "$goal_json" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$path"
|
python3 "$root/shared/hooks/scripts/session-state.py" "${arguments[@]}" "$@"
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SHELLS = [
|
||||||
|
('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||||
|
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||||
|
if os.name == 'nt' else shutil.which('bash')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStateHookTests(unittest.TestCase):
|
||||||
|
def test_writer_migrates_legacy_state_and_requires_reset_for_new_goal(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
workspace = Path(directory)
|
||||||
|
state_file = workspace / '.ai-session/state.json'
|
||||||
|
state_file.parent.mkdir()
|
||||||
|
state_file.write_text(json.dumps({'goal': 'Original', 'changedFiles': ['a'],
|
||||||
|
'pending': ['b'], 'importantFindings': ['c']}))
|
||||||
|
command = [sys.executable, str(ROOT / 'shared/hooks/scripts/session-state.py'),
|
||||||
|
'--workspace', str(workspace)]
|
||||||
|
result = subprocess.run(command + ['--verified', 'passed'], capture_output=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
state = json.loads(state_file.read_text())
|
||||||
|
self.assertEqual(state['changed'], ['a'])
|
||||||
|
self.assertEqual(state['open'], ['b'])
|
||||||
|
self.assertEqual(state['decisions'], ['c'])
|
||||||
|
self.assertFalse({'changedFiles', 'pending', 'importantFindings'} & state.keys())
|
||||||
|
before = state_file.read_bytes()
|
||||||
|
result = subprocess.run(command + ['--goal', 'New'], capture_output=True)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(state_file.read_bytes(), before)
|
||||||
|
result = subprocess.run(command + ['--reset', '--goal', 'New'], capture_output=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
state = json.loads(state_file.read_text())
|
||||||
|
self.assertEqual(state['goal'], 'New')
|
||||||
|
self.assertEqual(state['verified'], [])
|
||||||
|
self.assertEqual(state['changed'], [])
|
||||||
|
state_file.write_text('{"goal":"New","verified":"invalid"}')
|
||||||
|
before = state_file.read_bytes()
|
||||||
|
result = subprocess.run(command + ['--open', 'next'], capture_output=True)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(state_file.read_bytes(), before)
|
||||||
|
|
||||||
|
def test_writers_preserve_partial_updates_and_escape_values(self):
|
||||||
|
for shell, executable in SHELLS:
|
||||||
|
if not executable or not Path(executable).exists():
|
||||||
|
continue
|
||||||
|
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||||
|
base = Path(directory)
|
||||||
|
repository = base / 'repository'
|
||||||
|
shutil.copytree(ROOT / 'shared/hooks/scripts', repository / 'shared/hooks/scripts')
|
||||||
|
(repository / 'scripts').mkdir()
|
||||||
|
for filename in ('session-state.ps1', 'session-state.sh'):
|
||||||
|
shutil.copy(ROOT / 'scripts' / filename, repository / 'scripts' / filename)
|
||||||
|
workspace = base / 'workspace ü'
|
||||||
|
workspace.mkdir()
|
||||||
|
environment = dict(os.environ, TASK_WORKSPACE=str(workspace),
|
||||||
|
TASK_GOAL='Repair "quotes"\n' + 'x' * 300)
|
||||||
|
if shell == 'powershell':
|
||||||
|
prefix = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command']
|
||||||
|
commands = [
|
||||||
|
'& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Goal $env:TASK_GOAL -Decisions @("keep", "line two") -Pending @("check")',
|
||||||
|
'& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Verified @("passed")',
|
||||||
|
'& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Pointer',
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
prefix = [executable, '-c']
|
||||||
|
commands = [
|
||||||
|
'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --goal "$TASK_GOAL" --decisions keep "line two" --open check',
|
||||||
|
'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --verified passed',
|
||||||
|
'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --pointer',
|
||||||
|
]
|
||||||
|
for command in commands:
|
||||||
|
result = subprocess.run(prefix + [command], cwd=repository, capture_output=True,
|
||||||
|
encoding='utf-8', timeout=20, env=environment)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
state_file = workspace / '.ai-session/state.json'
|
||||||
|
self.assertIn(str(state_file), result.stdout)
|
||||||
|
state = json.loads(state_file.read_text(encoding='utf-8-sig'))
|
||||||
|
self.assertEqual(state['goal'], environment['TASK_GOAL'])
|
||||||
|
self.assertEqual(state['decisions'], ['keep', 'line two'])
|
||||||
|
self.assertEqual(state['open'], ['check'])
|
||||||
|
self.assertEqual(state['verified'], ['passed'])
|
||||||
|
self.assertEqual(state['workspace'], str(workspace))
|
||||||
|
self.assertFalse((repository / '.ai-session').exists())
|
||||||
|
self.assertEqual([p.name for p in state_file.parent.iterdir()], ['state.json'])
|
||||||
|
|
||||||
|
def run_hook(self, shell, executable, hook, config, workspace, payload):
|
||||||
|
names = {'pointer': ('Show-SessionStatePointer.ps1', 'show-session-state-pointer.sh'),
|
||||||
|
'compact': ('Record-Compact.ps1', 'record-compact.sh')}
|
||||||
|
script = config / 'hooks/scripts' / names[hook][shell == 'bash']
|
||||||
|
command = ([executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', str(script)]
|
||||||
|
if shell == 'powershell' else [executable, str(script)])
|
||||||
|
result = subprocess.run(command, cwd=workspace, input=payload, capture_output=True,
|
||||||
|
encoding='utf-8', timeout=20)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(result.stderr, '')
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
def test_workspace_selection_and_read_only_state(self):
|
||||||
|
for shell, executable in SHELLS:
|
||||||
|
if not executable or not Path(executable).exists():
|
||||||
|
continue
|
||||||
|
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||||
|
base = Path(directory)
|
||||||
|
config = base / 'installed config'
|
||||||
|
shutil.copytree(ROOT / 'shared/hooks/scripts', config / 'hooks/scripts')
|
||||||
|
workspace = base / 'working project'
|
||||||
|
selected = base / 'selected project ü'
|
||||||
|
empty = base / 'empty project'
|
||||||
|
for folder in (config, workspace, selected, empty):
|
||||||
|
folder.mkdir(exist_ok=True)
|
||||||
|
for folder in (config, workspace, selected):
|
||||||
|
(folder / '.ai-session').mkdir()
|
||||||
|
(folder / '.ai-session/state.json').write_text('private state content')
|
||||||
|
for hook in ('pointer', 'compact'):
|
||||||
|
for payload, expected in (
|
||||||
|
('', workspace),
|
||||||
|
('{}', workspace),
|
||||||
|
(json.dumps({'cwd': str(selected)}, ensure_ascii=False), selected),
|
||||||
|
(json.dumps({'cwd': str(empty)}), None),
|
||||||
|
('{"cwd":"relative/path"}', None),
|
||||||
|
('{"cwd":null}', None),
|
||||||
|
('{"cwd":12}', None),
|
||||||
|
('{"cwd":""}', None),
|
||||||
|
('{bad json', None),
|
||||||
|
('[]', None),
|
||||||
|
):
|
||||||
|
with self.subTest(hook=hook, payload=payload):
|
||||||
|
output = self.run_hook(shell, executable, hook, config, workspace, payload)
|
||||||
|
if expected is None:
|
||||||
|
self.assertEqual(output, '')
|
||||||
|
else:
|
||||||
|
self.assertIn(str(expected / '.ai-session/state.json'), output)
|
||||||
|
self.assertNotIn(str(config), output)
|
||||||
|
self.assertNotIn('private state content', output)
|
||||||
|
for folder in (config, workspace, selected):
|
||||||
|
self.assertEqual((folder / '.ai-session/state.json').read_text(), 'private state content')
|
||||||
|
self.assertFalse((empty / '.ai-session').exists())
|
||||||
|
events = (config / '.ai-session/telemetry.jsonl').read_text(encoding='utf-8-sig').splitlines()
|
||||||
|
self.assertEqual(len(events), 10)
|
||||||
|
self.assertTrue(all(json.loads(line)['event'] == 'compact' for line in events))
|
||||||
|
|
||||||
|
def test_missing_workspace_state_succeeds_without_config_fallback(self):
|
||||||
|
for shell, executable in SHELLS:
|
||||||
|
if not executable or not Path(executable).exists():
|
||||||
|
continue
|
||||||
|
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||||
|
base = Path(directory)
|
||||||
|
config = base / 'config'
|
||||||
|
shutil.copytree(ROOT / 'shared/hooks/scripts', config / 'hooks/scripts')
|
||||||
|
(config / '.ai-session').mkdir()
|
||||||
|
(config / '.ai-session/state.json').write_text('{}')
|
||||||
|
workspace = base / 'workspace'
|
||||||
|
workspace.mkdir()
|
||||||
|
for hook in ('pointer', 'compact'):
|
||||||
|
self.assertEqual(self.run_hook(shell, executable, hook, config, workspace, ''), '')
|
||||||
|
self.assertEqual(list(workspace.iterdir()), [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -4,4 +4,5 @@ $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|||||||
$stateDir = Join-Path $root '.ai-session'
|
$stateDir = Join-Path $root '.ai-session'
|
||||||
New-Item $stateDir -ItemType Directory -Force | Out-Null
|
New-Item $stateDir -ItemType Directory -Force | Out-Null
|
||||||
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content (Join-Path $stateDir 'telemetry.jsonl')
|
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content (Join-Path $stateDir 'telemetry.jsonl')
|
||||||
exit 0
|
& python (Join-Path $PSScriptRoot 'session-state.py') --hook
|
||||||
|
exit $LASTEXITCODE
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param()
|
||||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
& python (Join-Path $PSScriptRoot 'session-state.py') --hook
|
||||||
if (Test-Path (Join-Path $root '.ai-session/state.json')) { Write-Output 'Task state is available in .ai-session/state.json; read it only if needed.' }
|
exit $LASTEXITCODE
|
||||||
@@ -3,3 +3,4 @@ set -euo pipefail
|
|||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
mkdir -p "$root/.ai-session"
|
mkdir -p "$root/.ai-session"
|
||||||
printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"
|
printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"
|
||||||
|
python3 "$root/hooks/scripts/session-state.py" --hook
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
def state_path(workspace):
|
||||||
|
if not isinstance(workspace, str) or not workspace or any(ord(c) < 32 for c in workspace):
|
||||||
|
raise ValueError('Invalid workspace')
|
||||||
|
root = Path(workspace)
|
||||||
|
if not root.is_absolute() or not root.is_dir():
|
||||||
|
raise ValueError('Invalid workspace')
|
||||||
|
root = root.resolve()
|
||||||
|
directory = root / '.ai-session'
|
||||||
|
path = directory / 'state.json'
|
||||||
|
if directory.is_symlink() or path.is_symlink() or directory.resolve().parent != root:
|
||||||
|
raise ValueError('Invalid state path')
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def show_pointer(path):
|
||||||
|
if path.is_file():
|
||||||
|
print('Task state is available at ' + str(path) + '; validate its workspace and goal before reuse.')
|
||||||
|
|
||||||
|
|
||||||
|
def hook_pointer():
|
||||||
|
try:
|
||||||
|
raw = '' if sys.stdin.isatty() else sys.stdin.buffer.read(1048577).decode('utf-8-sig')
|
||||||
|
if len(raw) > 1048576:
|
||||||
|
return
|
||||||
|
event = json.loads(raw) if raw.strip() else {}
|
||||||
|
if not isinstance(event, dict):
|
||||||
|
return
|
||||||
|
show_pointer(state_path(event.get('cwd', os.getcwd())))
|
||||||
|
except (ValueError, OSError, RecursionError):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def update_state(args):
|
||||||
|
path = state_path(args.workspace)
|
||||||
|
if args.pointer:
|
||||||
|
show_pointer(path)
|
||||||
|
return
|
||||||
|
state = json.loads(path.read_text(encoding='utf-8-sig')) if path.exists() else {}
|
||||||
|
if not isinstance(state, dict):
|
||||||
|
raise ValueError('Invalid task state')
|
||||||
|
workspace = str(path.parent.parent)
|
||||||
|
if state.get('workspace', workspace) != workspace:
|
||||||
|
raise ValueError('Task state belongs to another workspace')
|
||||||
|
for old, new in (('changedFiles', 'changed'), ('pending', 'open'), ('importantFindings', 'decisions')):
|
||||||
|
if old in state:
|
||||||
|
state.setdefault(new, state.pop(old))
|
||||||
|
for field in ('goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
||||||
|
if field not in state:
|
||||||
|
continue
|
||||||
|
value = state[field]
|
||||||
|
if field == 'goal':
|
||||||
|
valid = isinstance(value, str)
|
||||||
|
else:
|
||||||
|
valid = isinstance(value, list) and all(isinstance(item, str) for item in value)
|
||||||
|
if not valid:
|
||||||
|
raise ValueError('Invalid task state field')
|
||||||
|
if args.reset:
|
||||||
|
state = {}
|
||||||
|
elif args.goal is not None and state.get('goal') and state['goal'] != args.goal:
|
||||||
|
raise ValueError('A different goal requires reset')
|
||||||
|
state['workspace'] = workspace
|
||||||
|
for field in ('goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
||||||
|
value = getattr(args, field)
|
||||||
|
if value is not None:
|
||||||
|
state[field] = value
|
||||||
|
else:
|
||||||
|
state.setdefault(field, '' if field == 'goal' else [])
|
||||||
|
state['updatedAt'] = datetime.now(timezone.utc).isoformat()
|
||||||
|
path.parent.mkdir(exist_ok=True)
|
||||||
|
temporary = None
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=path.parent,
|
||||||
|
prefix='.state-', suffix='.tmp', delete=False) as output:
|
||||||
|
temporary = output.name
|
||||||
|
json.dump(state, output, ensure_ascii=False, indent=2)
|
||||||
|
output.write('\n')
|
||||||
|
os.replace(temporary, path)
|
||||||
|
temporary = None
|
||||||
|
finally:
|
||||||
|
if temporary is not None:
|
||||||
|
os.unlink(temporary)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--hook', action='store_true')
|
||||||
|
parser.add_argument('--update-json', action='store_true')
|
||||||
|
parser.add_argument('--workspace', default=os.getcwd())
|
||||||
|
parser.add_argument('--pointer', action='store_true')
|
||||||
|
parser.add_argument('--reset', action='store_true')
|
||||||
|
parser.add_argument('--goal')
|
||||||
|
for field in ('decisions', 'changed', 'verified', 'open', 'risks'):
|
||||||
|
parser.add_argument('--' + field, nargs='*')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.hook:
|
||||||
|
hook_pointer()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if args.update_json:
|
||||||
|
values = json.loads(sys.stdin.buffer.read().decode('utf-8-sig'))
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
raise ValueError('Invalid task state update')
|
||||||
|
for field, value in values.items():
|
||||||
|
if field not in ('workspace', 'goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
||||||
|
raise ValueError('Invalid task state field')
|
||||||
|
if field in ('workspace', 'goal'):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ValueError('Invalid task state value')
|
||||||
|
elif not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
||||||
|
raise ValueError('Invalid task state list')
|
||||||
|
setattr(args, field, value)
|
||||||
|
update_state(args)
|
||||||
|
except (ValueError, OSError):
|
||||||
|
parser.exit(1, 'Cannot read or update task state for this workspace.\n')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||||
[ -f "$root/.ai-session/state.json" ] && printf 'Task state is available in .ai-session/state.json; read it only if needed.\n'
|
python3 "$script_dir/session-state.py" --hook
|
||||||
Reference in new issue
Block a user